Build a simple web app

This tutorial goes through all the steps necessary for you to create a simple web app from scratch, that'll integrate with your Qlik Sense SaaS tenant. You'll learn about things and concepts such as:

If you're just interested in the code produced in this tutorial, skip to the Summary section.

Note: You need access to a Qlik Sense SaaS tenant, and either tenant administrator rights yourself, or the means to get an web integration set up for you. You can sign up for a free trial if you dont have an existing tenant.

Requirements

This tutorial, requires you to have the following things accessible.

You need:

  • Node.js (version 10 or newer)
  • A terminal (for example Git Bash on Windows, or Terminal.app on Mac)
  • OpenSSL (installed through Git Bash, and by default on Mac)
  • A modern web browser, for example Google Chrome
  • A text editor or IDE of your choice, for example Visual Studio Code
  • An existing web integration, or possibility to get one created in your tenant

Bootstrapping the project

To avoid making this tutorial too advanced, you'll only work with plain HTML, CSS, JavaScript, except for the enigma.js library which simplifies communication with the Qlik Associative Engine through websockets.

But before you can start writing any code, you need to prepare the project. There are three steps needed:

  • Create the necessary project (folders and files) structure
  • Start a local web server
  • Create or retrieve a web integration configuration from your tenant

Project structure

First of all, start your preferred terminal and create the project folder.

# move to a folder where you want to store the project:
cd ~

# new folder to contain the project files:
mkdir simple-web-app

But what good is a folder without any content? Continue by creating an index page and an app JavaScript file that serves as the basis for the logic needed to communicate with your tenant.

Create an index.html file using your favorite editor and save the file in the project folder.

<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Simple web app</title>
<div id="intro">Loading...</div>
<div id="title">Loading...</div>
<script src="https://unpkg.com/enigma.js@2.7.0/enigma.min.js"></script>
<script src="app.js"></script>

</html>

Next, create the app.js file. Just put an IIFE expression in that file for now, you'll fill it out with more content further into this tutorial.

(async () => {
})();

Next up, learn how to start the HTTPS web server needed for communication with your tenant.

Starting the web server

To serve your web app locally, you'll need a HTTPS web server. The web integration that you'll set up later requires HTTPS, which means that you can't just open up your web app files using the file:// protocol.

In this tutorial you'll use http-server, a package available through the npm package manager that comes with your Node.js installation. This is a simple command-line web server that also support HTTPS.

Open up your terminal, and enter the following commands. Ignore typing the lines starting with # as they're just comments for each command. The npx command comes as part of your npm installation and allows you to execute command-line tools available on npm without having to install them globally.

# move to the folder creating during the `Project structure` section:
cd ~/simple-web-app

# create a certificate needed for the HTTPS server, note that this command
# may prompt you for additional information, enter the information needed
# and continue:
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
# if this command fails, you may have to install openssl manually first.

# start the HTTPS server using the certificates generated before.
# -S means to use HTTPS and the certificates generated by openssl, -c -1 disabled caching:
npx http-server -S -c -1

If these commands run successfully, you should now have a HTTPS web server running. Note the URL shown in the command-line window as you'll need it in the next step when configuring the web integration.

Create or retrieve web integration

To allow third-party domains to communicate with your tenant, you need to set up a web integration. This process requires tenant administrator privileges.

Once you've created, or retrieved, the web integration id you should now have everything needed to start communicating with your tenant in your web app.

In the next section, you'll learn more about the business logic needed in your web app to communicate with your tenant.

Building the web app

In the previous section you learned how to set up your developer environment, including retrieving the necessary web integration id. In this section, you'll learn:

  • How to configure your HTTP requests properly
  • How to detect if your web app visitor is logged in and trigger the single-sign if needed
  • How to open up websocket connections to Qlik Associative Engine using enigma.js

This section implements the concepts covered in the Build web solutions, that page covers for example how the single-sign on flow works, how to configure a web integration, and the security measures you need to be aware of.

Configure HTTP requests

When communicating with a tenant, you're required to supply the web integration you've configured for your web app with each request. This reduces the risk of abuse, and at the same time avoids having to maintain a global whitelist of domains for each tenant.

Next, you'll add some code to your app.js file that helps you do properly configured requests towards your tenant.

Note: fetch is a web standards API for making REST calls, see full documentation on this API.

(async () => {
  const tenantUri = 'https://your-tenant.us.qlikcloud.com';
  const webIntegrationId = 'your-web-integration-id';
  const titleElement = document.getElementById('title');

  async function request(path, returnJson = true) {
    const res = await fetch(`${tenantUri}${path}`, {
      mode: 'cors',
      credentials: 'include',
      redirect: 'follow',
      headers: {
        // web integration is sent as a header:
        'qlik-web-integration-id': webIntegrationId,
      },
    });
    return returnJson ? res.json() : res;
  }
})();

This function is used throughout the rest of this tutorial to simplify doing REST API calls. fetch has to be configured for CORS, or your web app might be blocked by the browser and/or web server. Note that your web integration id is sent as a header.

Ensure your user is logged in to your tenant

In this step, you'll fetch the user metadata, and trigger the single-sign on flow if your user isn't logged into your tenant.

Preceding })(); in your app.js file, add the following code.

  try {
    // call your-tenant.us.qlikcloud.com/api/v1/users/me to
    // retrieve the user metadata, as a way to detect if they
    // are logged in. An error will be thrown if the response
    // is a non-2XX HTTP status:
    const user = await request('/api/v1/users/me');
    document.getElementById('intro').innerHTML = `Hello, ${user.name}`;
  } catch (err) {
    const returnTo = encodeURIComponent(window.location.href);
    // redirect your user to the tenant log in screen, and once they're
    // logged in, return to your web app:
    window.location.href = `${tenantUri}/login?returnto=${returnTo}&qlik-web-integration-id=${webIntegrationId}`;
  }

Save the app.js file, and refresh your browser and you should now see a Hello, <your name> message once you've logged in.

Note: If you get sent to the hub after logging in, it's likely that your web integration is misconfigured. Doublecheck for example that you're using the correct web integration id, and that it has the origin your solution runs on whitelisted in it.

Next up, you'll fetch the CSRF token which is required as a header in all non-GET requests, and for websockets it is defined as a query parameter instead (since you can't set headers on websockets in browser environments).

Preceding })(); in your app.js file, add the following code.

  try {
    // fetch the CSRF token:
    const res = await request('/api/v1/csrf-token', false);
    const csrfToken = res.headers.get('qlik-csrf-token');
  } catch (err) {
    window.console.log('Error while setting up:', err);
  }

Note: It's using the request function created earlier, and the false parameter ensures that you'll get access to the basic response object instead of the JSON body, which is needed to access the headers in this case.

Open an app using enigma.js

In the previous step, you stored the csrfToken value which is needed when opening websockets. The next thing now is to open a session towards an app using enigma.js. The use case is simplified here so feel free to play around with the code to fit your needs once you got it to present the app title in your web app.

After the const csrfToken = ... row in in your app.js file, add the following code.

    // fetch the list of available apps:
    const apps = await request('/api/v1/items?resourceType=app');

    if (!apps.data.length) {
      titleElement.innerHTML = 'No apps available';
      return;
    }

Note: If your web app is displaying the No apps available text, it means that the user you're logged in as doesn't have any apps available to them yet. Head over to the hub and create or import an app then continue this tutorial.

Once you have the app list, you can continue by creating the enigma.js session.

After the if-clause you added from the previous step, add the following code in your app.js file.

    // grab the first app id in the list:
    const appId = apps.data[0].resourceId;

    // build a websocket URL:
    const url = `${tenantUri.replace(
      'https',
      'wss'
    )}/app/${appId}?qlik-web-integration-id=${webIntegrationId}&qlik-csrf-token=${csrfToken}`;

    // fetch the engine API schema:
    const schema = await (
      await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')
    ).json();

    // create the enigma.js session:
    const session = window.enigma.create({ url, schema });

In the URL, note that you're referring to the appId, webIntegrationId, and csrfToken values. These are all required parameters for opening a websocket properly. Next up is to actually open the websocket, and the app, and finally display the app title in your web app.

After the const session = ... row, add the following code to your app.js file.

    const global = await session.open();

    // open the app, and fetch the layout:
    const app = await global.openDoc(appId);
    const appLayout = await app.getAppLayout();

    // finally, present the app title in your web app:
    titleElement.innerHTML = appLayout.qTitle;

This concludes this section, you should now have your logged in users' name together with an app title presented in your web app.

Summary

This tutorial hopefully taught you how to create a basic web app from scratch that communicates with your tenant, and all the steps needed to configure web integrations, HTTP requests, and websockets.

The following files has been created as part of this tutorial.

index.html

<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Simple web app</title>
<div id="intro">Loading...</div>
<div id="title">Loading...</div>
<script src="https://unpkg.com/enigma.js@2.7.0/enigma.min.js"></script>
<script src="app.js"></script>

</html>

app.js

(async () => {
  const tenantUri = 'https://your-tenant.us.qlikcloud.com';
  const webIntegrationId = 'your-web-integration-id';
  const titleElement = document.getElementById('title');

  async function request(path, returnJson = true) {
    const res = await fetch(`${tenantUri}${path}`, {
      mode: 'cors',
      credentials: 'include',
      redirect: 'follow',
      headers: {
        // web integration is sent as a header:
        'qlik-web-integration-id': webIntegrationId,
      },
    });
    return returnJson ? res.json() : res;
  }

  try {
    // call your-tenant.us.qlikcloud.com/api/v1/users/me to
    // retrieve the user metadata, as a way to detect if they
    // are logged in. An error will be thrown if the response
    // is a non-2XX HTTP status:
    const user = await request('/api/v1/users/me');
    document.getElementById('intro').innerHTML = `Hello, ${user.name}`;
  } catch (err) {
    const returnTo = encodeURIComponent(window.location.href);
    // redirect your user to the tenant log in screen, and once they're
    // logged in, return to your web app:
    window.location.href = `${tenantUri}/login?returnto=${returnTo}&qlik-web-integration-id=${webIntegrationId}`;
  }

  try {
    // fetch the CSRF token:
    const res = await request('/api/v1/csrf-token', false);
    const csrfToken = res.headers.get('qlik-csrf-token');

    // fetch the list of available apps:
    const apps = await request('/api/v1/items?resourceType=app');

    if (!apps.data.length) {
      titleElement.innerHTML = 'No apps available';
      return;
    }

    // grab the first app id in the list:
    const appId = apps.data[0].resourceId;

    // build a websocket URL:
    const url = `${tenantUri.replace(
      'https',
      'wss'
    )}/app/${appId}?qlik-web-integration-id=${webIntegrationId}&qlik-csrf-token=${csrfToken}`;

    // fetch the engine API schema:
    const schema = await (
      await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')
    ).json();

    // create the enigma.js session:
    const session = window.enigma.create({ url, schema });
    const global = await session.open();

    // open the app, and fetch the layout:
    const app = await global.openDoc(appId);
    const appLayout = await app.getAppLayout();

    // finally, present the app title in your web app:
    titleElement.innerHTML = appLayout.qTitle;
  } catch (err) {
    window.console.log('Error while setting up:', err);
  }
})();

If you have feedback, questions, comments, criticism, head over to the Qlik Community and discuss.