Skip to main content
Announcements
Introducing Qlik Answers: A plug-and-play, Generative AI powered RAG solution. READ ALL ABOUT IT!
Ouadie
Employee
Employee

In my previous blog posts (part 1, part 2), I explained how we can use enigma.js to communicate with the Qlik Associative Engine and get access to data. We also went through the concept of Generic Objects and saw how they can be used to do many things including getting raw data and using it to build visualizations.

In this post, we are going to expand on that and take a look at a real world example where enigma.js can be used to get Master Measures data that is rendered as KPIs in a web app, and monitor them for any changes to reflect the latest values.

This post is based on the following tutorial on qlik.dev where you can find the boilerplate code and more resources to help you get started: https://qlik.dev/embed/control-the-experience/dimensions-and-measures/get-master-measures/

You will find the full example code attached at the end of the post, I recommend you download it and open it in your favorite text editor as I will only feature some parts of it to keep this post short.

1- First, let’s take a look at the index.html:

  • We include the enigma.js library.
  • We define the configuration options to connect to our Qlik Cloud tenant and other needed variables including:
    • the tenant URL
    • the Web Integration ID (you can learn more about how to create this here)
    • The App ID
    • and a list containing the names of the Master Measures we wish to access.

 

const TENANT = '<INSERT YOUR TENANT HERE (example: xxxx.us.qlikcloud.com)>';
const WEB_INTEGRATION_ID = '<INSERT WEB INTEGRATION ID HERE>';
const APP_ID = '<INSERT APP ID HERE>';
const MASTER_MEASURE_NAMES = ['# of Invoices', 'Average Sales per Invoice', 'Sales (LYTD)', 'Sales LY'];
const IDENTITY = '1234';

 

  • In the main function, we initiate the login process, get the csrf token, and open the Enigma session. Then we get all the Master Measures via the getMeasureList function, and render only the Master Measure data from the "MASTER_MEASURE_NAMES" list we previously defined.
  • All the functions are defined in scripts.js

 

(async function main() {
    const isLoggedIn = await qlikLogin();
    const qcsHeaders = await getQCSHeaders();
    const [session, enigmaApp] = await getEnigmaSessionAndApp(qcsHeaders, APP_ID, IDENTITY);

    handleDisconnect(session);

    const allMasterMeasuresList = await getMeasureList(enigmaApp);
    const masterMeasureValuesDct = await masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, MASTER_MEASURE_NAMES);
})();

 

2- Now, let’s take a look at the different functions that make this happen:

  • Login and session handling:
    • the qlikLogin function checks to see if you are login by fetching the /api/v1/users/me api endpoint, if not it redirects to the Interactive idp login page.
    • getQCSHeaders fetches the CSRF token needed to make the websocket connection to the Qlik Engine.

 

// LOGIN
async function qlikLogin() {
    const loggedIn = await fetch(`https://${TENANT}/api/v1/users/me`, {
        mode: 'cors',
        credentials: 'include',
        headers: {
            'qlik-web-integration-id': WEB_INTEGRATION_ID,
        },
    })
    if (loggedIn.status !== 200) {
        if (sessionStorage.getItem('tryQlikAuth') === null) {
            sessionStorage.setItem('tryQlikAuth', 1);
            window.location = `https://${TENANT}/login?qlik-web-integration-id=${WEB_INTEGRATION_ID}&returnto=${location.href}`;
            return await new Promise(resolve => setTimeout(resolve, 10000)); // prevents further code execution
        } else {
            sessionStorage.removeItem('tryQlikAuth');
            const message = 'Third-party cookies are not enabled in your browser settings and/or browser mode.';
            alert(message);
            throw new Error(message);
        }
    }
    sessionStorage.removeItem('tryQlikAuth');
    console.log('Logged in!');
    return true;
}

// QCS HEADERS
async function getQCSHeaders() {
    const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
        mode: 'cors',
        credentials: 'include',
        headers: {
            'qlik-web-integration-id': WEB_INTEGRATION_ID
        },
    })
    const csrfToken = new Map(response.headers).get('qlik-csrf-token');
    return {
        'qlik-web-integration-id': WEB_INTEGRATION_ID,
        'qlik-csrf-token': csrfToken,
    };
}

 

 

  • Enigma session connection:
    • we use enigma.create() function to establish the websocket connection and create a new QIX session.
    • we use openDoc() method of the global object to open our app, and then return it for later use.

 

// ENIGMA ENGINE CONNECTION
async function getEnigmaSessionAndApp(qcsHeaders, appId, identity) {
    const params = Object.keys(qcsHeaders)
        .map((key) => `${key}=${qcsHeaders[key]}`)
        .join('&');
    return (async () => {
        const schema = await (await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')).json();
        try {
            return await createEnigmaAppSession(schema, appId, identity, params);
        }
        catch {
            const waitSecond = await new Promise(resolve => setTimeout(resolve, 1500));
            try {
                return await createEnigmaAppSession(schema, appId, identity, params);
            }
            catch (e) {
                throw new Error(e);
            }
        }
    })();
}
async function createEnigmaAppSession(schema, appId, identity, params) {
    const session = enigma.create({
        schema,
        url: `wss://${TENANT}/app/${appId}/identity/${identity}?${params}`
    });
    const enigmaGlobal = await session.open();
    const enigmaApp = await enigmaGlobal.openDoc(appId);
    return [session, enigmaApp];
}

 

  • Get a list of all master measures in our app:
    Now that we have the enigma app object, we can use the createSessionObject method to create a session object by passing the qMeasureListDef definition with qType “measure”

 

// GET LIST OF ALL MASTER MEASURES
async function getMeasureList(enigmaApp) {
    const measureListProp = {
        "qInfo": {
            "qType": "MeasureList",
            "qId": ""
        },
        "qMeasureListDef": {
            "qType": "measure",
            "qData": {
                "title": "/qMetaDef/title",
                "tags": "/qMetaDef/tags"
            }
        }
    }

    const measureListObj = await enigmaApp.createSessionObject(measureListProp);
    const measureList = await measureListObj.getLayout();

    return measureList.qMeasureList.qItems;
}

 

  • Get data from our list of Master Measures:
    • Now, we loop through the list of all master measures returned from the function above and only grab the ones matching our list of matching measures from the MASTER_MEASURE_NAMES variable defined in index.html.
    • We then create a generic object based on the Hypercube definition that includes the matchingMeasures representing the measureObjects’ qIds.
    • Finally, we listen to any changes using the .on(”changed” …) event listener and grab the latest layout.

 

// CREATE HYPERCUBE WITH MULTIPLE MASTER MEASURES (INCLUDE MATCHING NAMES ONLY)
async function masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, desiredMasterMeasureNamesList) {

    let matchingMeasures = [];
    allMasterMeasuresList.forEach(measureObject => {
        if (desiredMasterMeasureNamesList.includes(measureObject.qMeta.title)) {
            matchingMeasures.push({
                "qLibraryId": measureObject.qInfo.qId
            })
        }
    });

    if (!matchingMeasures.length > 0) {
        console.log('No matching master measures found! Exiting...');
        return
    }

    const measureDef = {
        "qInfo": {
            "qType": 'hypercube',
        },
        "qHyperCubeDef": {
            "qDimensions": [],
            "qMeasures": matchingMeasures,
            "qInitialDataFetch": [
                {
                    "qHeight": 1,
                    "qWidth": matchingMeasures.length,
                },
            ],
        },
    };

    const measureObj = await enigmaApp.createSessionObject(measureDef);
    const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;

    // LISTEN FOR CHANGES AND GET UPDATED LAYOUT
    measureObj.on('changed', async () => {
        const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;
        processAndPlotMeasureHypercube(measureObjHypercube);
    })

    processAndPlotMeasureHypercube(measureObjHypercube);
}

 

  • Render the data to the HTML as KPIs:
    Lastly, we retrieve the data in “hypercube.qDataPages[0].qMatrix” and loop through it to construct an easy manipulate array of key/value objects which are then injected into the HTML.

 

//    HELPER FUNCTION TO PROCESS HYPERCUBE INTO USER FRIENDLY DICTIONARY
function processAndPlotMeasureHypercube(hypercube) {

    const masterMeasureValuesDict = Object.create(null);

    hypercube.qMeasureInfo.forEach((measure, i) => {
        masterMeasureValuesDict[measure.qFallbackTitle] = hypercube.qDataPages[0].qMatrix[0][i].qText;
    });

    const masterMeasureKeys = Object.keys(masterMeasureValuesDict);
    masterMeasureKeys.sort();

    const sortedMasterMeasureValuesDict = Object.create(null);
    masterMeasureKeys.forEach(name => {
        sortedMasterMeasureValuesDict[name] = masterMeasureValuesDict[name];
    })

    renderKpis(sortedMasterMeasureValuesDict);
}


// RENDER KPIs
function renderKpis(masterMeasureValuesDict) {

    let kpiData = [];
    Object.entries(masterMeasureValuesDict).forEach(([key, value]) => {
        kpiData.push({
            label: key,
            value: Number(value).toLocaleString()
        });
    });

    const kpisContainer = document.querySelector('#kpis');
    kpisContainer.innerHTML = '';

    kpiData.forEach((kpi) => {
        const kpiCard = document.createElement('div');
        kpiCard.classList.add('kpi-card');

        const labelElement = document.createElement('div');
        labelElement.classList.add('kpi-label');
        labelElement.innerText = kpi.label;

        const valueElement = document.createElement('div');
        valueElement.classList.add('kpi-value');
        valueElement.innerText = kpi.value;

        kpiCard.appendChild(labelElement);
        kpiCard.appendChild(valueElement);

        kpisContainer.appendChild(kpiCard);
    });
}

 


This is how the KPIs are rendered to the page:

img1--before.png

To show how the on-change event listener works, let's simulate a change by editing the # of Invoices Master Measure in Qlik:

img-1.png

Looking back the web page, the change is instantly reflected on the KPI:

img1--after.png

That’s all, I hope you found this post helpful, do not forget to check out more tutorials on qlik.dev that cover other important use cases!

P.S: to make it easier to run the web app, I have included a server.py file to easily serve the files via https://localhost:8000. You can run it with the command: python server.py.

Also, don’t forget to whitelist this localhost domain when generating a new Web Integration ID:

img-2.png

Thanks for reading!