Skip to main content
Woohoo! Qlik Community has won “Best in Class Community” in the 2024 Khoros Kudos awards!
Announcements
Nov. 20th, Qlik Insider - Lakehouses: Driving the Future of Data & AI - PICK A SESSION
cancel
Showing results for 
Search instead for 
Did you mean: 
pixlics12
Contributor III
Contributor III

How to embed Qlik App/Sheet on the website

I want to embed Qlik app/sheet on my website. There are apps on the Qlik, and the apps contains the sheets, which I want to embed for showing to website visitors/user. But users don't have access of Qlik, so I need to embed with some kind of authentication by code.

Before going on the code, I have following things created on the Qlik Management Console (QMC):

  1. Tenant host
  2. Added my domain in the Content Security Policy section and selected frame-ancestors in the Directive
  3. Web Integration Id created in Web Integration Configuration section
  4. Client Id and secret created in OAuth section
  5. Public key, Private key created by following this article: Create Signed Tokens for JWT Authorization
  6. After following above article, I also have Issuer, Key ID which I setup in the Identity Provider section

Following is my usecase

User first select an app from the dropdown, this list populated through the following API:

GET https://QLIK-TENANT/api/v1/items?resourceType=app
Header: Bearer <token generated with OAuth Client id and secret>

And it is working.

After that I need to show another dropdown for all the sheets of the selected app. But I am unable to fetch the list of sheets. I checked there is no REST API which can give me sheets by an app.

Question 1: How can I get sheet list of the app? I also plan to list the sheets using below JS code, but that is also not working.

Once user select app and sheet, then click on a button to Render sheet, and it should render that sheet on the webpage.

I tried using below piece of code (enigma js), referenced from here: Handle sheets in iframes with enigma.js

<html>
<head>
<script src="https://unpkg.com/enigma.js/enigma.min.js"></script>
</head>
<body>
    <div id="main">
        <div id="message"></div>
        <iframe id="qlik_frame" style="border:none;width:100%;height:900px;"></iframe>
    </div>
    <script type="text/javascript">
        // CONFIGURATION
        const TENANT = "xxxxxxxxxxxxx.xx.qlikcloud.com";
        const JWTENDPOINT = "https://example.com/api/v1/qlik/jwtToken"; // this endpoint create a JWT token using passed data and the private key mentioned in point #6 above
        const WEBINTEGRATIONID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // mentioned in point #3 above
        const APPID = "411111e0-36f8-4e6d-b25d-f0xxxxxxxxca"; // got using the REST API mentioned above
        const IDENTITY = "xxxxxxxxxxxxxx"; // An arbitrary string to establish a separate session state.

        // MAIN
        (async function main() {
            const isLoggedIn = await qlikLogin();
            const qcsHeaders = await getQCSHeaders();
            const [session, enigmaApp] = await connectEnigma(qcsHeaders, APPID, IDENTITY);

            handleDisconnect(session);
            const theme = await getTheme(enigmaApp);
            const spaceId = (await getApp(APPID)).spaceId;
            const spaceType = await getSpaceType(spaceId);
            const sheets = await getSheetList(enigmaApp, spaceType);

            renderSingleIframe("qlik_frame", APPID, sheets[0].qInfo.qId, theme, IDENTITY);
        })();

        // LOGIN
        async function qlikLogin() {
            const loggedIn = await checkLoggedIn();
            if (loggedIn.status !== 200) {
                const $jwtPostData = {
                    kid: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // mentioned in point #6 above
                    iss: "xxxxxxxxxxxxx.xx.qlikcloud.com", 
                    name: "?????????" // what it should be????
                };
                const tokenRes = await (await getJWTToken(JWTENDPOINT, $jwtPostData));
                const loginRes = await jwtLogin(tokenRes.body);

                if (loginRes.status != 200) {
                    const message = "Something went wrong while logging in.";
                    throw new Error(message);
                }
                const recheckLoggedIn = await checkLoggedIn();
                if (recheckLoggedIn.status !== 200) {
                    const message = "Third-party cookies are not enabled in your browser settings and/or browser mode.";
                    throw new Error(message);
                }
            }
            return true;
        }

        async function checkLoggedIn() {
            return await fetch(`https://${TENANT}/api/v1/users/me`, {
                mode: "cors",
                credentials: "include",
                headers: {
                    "qlik-web-integration-id": WEBINTEGRATIONID
                },
            })
        }

        // Get Method the JWT and use it to obtain Qlik Cloud session cookie.
        async function getJWTToken(jwtEndpoint, payloadData) {
            try {
                // Define the headers for the request, including the content type
                const headers = new Headers({
                    "Content-Type": "application/json",
                });

                // Create a request object with the specified method, headers, and body
                const requestOptions = {
                    method: "POST",
                    headers: headers,
                    mode: "cors",
                    body: JSON.stringify(payloadData), // Convert payloadData to JSON
                };

                // Use the fetch function to make a POST request to the specified endpoint
                const response = await fetch(jwtEndpoint, requestOptions);

                // Check if the response status code indicates success (e.g., 200 OK)
                if (response.ok) {
                    // Parse the response body as JSON, assuming it contains the JWT
                    const jwtData = await response.json();
                    return jwtData; // Return the JWT data
                } else {
                    // Handle HTTP error responses here if needed
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }
            } catch (error) {
                // Handle any exceptions that may occur during the fetch request
                console.error("Error fetching JWT:", error);
                throw error;
            }
        }

        async function jwtLogin(token) {
            const authHeader = `Bearer ${token}`;
            return await fetch(`https://${TENANT}/login/jwt-session?qlik-web-integration-id=${WEBINTEGRATIONID}`, {
                credentials: "include",
                mode: "cors",
                method: "POST",
                headers: {
                    "Authorization": authHeader,
                    "qlik-web-integration-id": WEBINTEGRATIONID
                },
            })
        }

        async function getQCSHeaders() {
            const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
                mode: "cors",
                credentials: "include",
                headers: {
                    "qlik-web-integration-id": WEBINTEGRATIONID
                },
            })

            const csrfToken = new Map(response.headers).get("qlik-csrf-token");
            return {
                "qlik-web-integration-id": WEBINTEGRATIONID,
                "qlik-csrf-token": csrfToken,
            };
        }

        // ENIGMA ENGINE CONNECTION
        async function connectEnigma(qcsHeaders, appId, identity) {
            const [session, app] = await getEnigmaSessionAndApp(appId, qcsHeaders, identity);
            return [session, app];
        }

        async function getEnigmaSessionAndApp(appId, headers, identity) {
            const params = Object.keys(headers)
                .map((key) => `${key}=${headers[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 {
                    // If the socket is closed immediately following the connection this
                    // could be due to an edge-case race condition where the newly created
                    // user does not yet have access to the app due to access control propagation.
                    // This bit of code will make another attempt after a 1.5 seconds.
                    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}`,
                createSocket: url => new WebSocket(`wss://${TENANT}/app/${appId}/identity/${identity}?${params}`),
            });

            const enigmaGlobal = await session.open();
            const enigmaApp = await enigmaGlobal.openDoc(appId);
            return [session, enigmaApp];
        }

        // HANDLE ENGINE SESSION CLOSURE
        function handleDisconnect(session) {
            session.on("closed", () => {
                console.log("Due to inactivity or loss of connection, this session has ended.");
            });

            session.on("suspended", () => {
                console.log("Due to loss of connection, this session has been suspended.");
            });

            window.addEventListener("offline", () => {
                session.close();
            });
        }

        // GET QLIK APP (FOR SPACE ID)
        async function getApp(appId) {
            var url = new URL(`https://${TENANT}/api/v1/items?resourceType=app&resourceId=${appId}`);
            const response = await fetch(url, {
                method: "GET",
                mode: "cors",
                credentials: "include",
                headers: {
                    "Content-Type": "application/json",
                    "qlik-web-integration-id": WEBINTEGRATIONID,
                },
            })
            responseJson = await response.json();
            return responseJson.data[0];
        }

        // GET SPACE (FOR SPACE TYPE)
        async function getSpaceType(spaceId) {
            var url = new URL(`https://${TENANT}/api/v1/spaces/${spaceId}`);
            const response = await fetch(url, {
                method: "GET",
                mode: "cors",
                credentials: "include",
                headers: {
                    "Content-Type": "application/json",
                    "qlik-web-integration-id": WEBINTEGRATIONID,
                },
            })
            responseJson = await response.json();
            return responseJson.type;
        }

        // GET THEME
        async function getTheme(enigmaApp) {
            const createAppProps = await enigmaApp.createSessionObject({
                qInfo: {
                    qId: "AppPropsList",
                    qType: "AppPropsList"
                },
                qAppObjectListDef: {
                    qType: "appprops",
                    qData: {
                        theme: "/theme"
                    }
                }
            });
            const appProps = await enigmaApp.getObject("AppPropsList");
            const appPropsLayout = await appProps.getLayout();
            const theme = appPropsLayout.qAppObjectList.qItems[0].qData.theme;
            return theme;
        }

        // GET SHEETS (WITH TYPE ADDED, E.G., BASE, COMMUNITY, PRIVATE)
        async function getSheetList(app, spaceType) {
            var sheets = await app.getObjects({
                "qOptions": {
                    "qTypes": [
                        "sheet"
                    ],
                    "qIncludeSessionObjects": false,
                    "qData": {}
                }
            })
            sheetsIncludingType = [];
            for await (const sheet of sheets) {
                var pushSheet = true;
                const sheetObject = await app.getObject(sheet.qInfo.qId);
                const sheetLayout = await sheetObject.getLayout();
                var isManaged = spaceType === "managed";
                var sheetTypeEnum = 1;
                var approved = sheet.qMeta.approved;
                var published = sheet.qMeta.published;
                const sheetTypeEnums = {
                    1: "Base",
                    2: "Community",
                    3: "Private"
                };
                if (!approved && !published) {
                    sheetTypeEnum = 3;
                } else if (!approved && published) {
                    if (isManaged) {
                        sheetTypeEnum = 2;
                    }
                }
                const sheetTypeObject = {
                    "sheetType": sheetTypeEnums[sheetTypeEnum],
                    "sheetTypeEnum": sheetTypeEnum
                };
                const mergedObject = {
                    ...sheetLayout,
                    ...sheetTypeObject
                };
                sheetsIncludingType.push(mergedObject)
            }
            sheetsIncludingType.sort((a, b) => (a.sheetTypeEnum - b.sheetTypeEnum || a.rank - b.rank))
            return sheetsIncludingType;
        }

        // HELPER FUNCTION TO GENERATE IFRAME
        function renderSingleIframe(frameId, appId, sheetId, theme, identity) {
            const frameUrl = `https://${TENANT}/single/?appid=${appId}&sheet=${sheetId}&theme=${theme}&identity=${identity}&opt=ctxmenu,currsel`;
            document.getElementById(frameId).setAttribute("src", frameUrl);
        }
    </script>
</body>
</html>

But when I tried to run, it is not working and returned following JS error in console, related to Socket:

console.log.png

 

Following is the network logs when I tried to run this code:

network.log.png

Question 2: How to embed sheet/app? In above code, Am I doing something wrong? Or if there are any other way to embed, please suggest me. Because I doubt, that it will need to create a lot of resources on the QMC. So there should be some API, to authenticate user, get token and use token to get embed code for the sheet.

Please help me. Any suggestion will also be helpful.

Thank you

Labels (3)
24 Replies
pixlics12
Contributor III
Contributor III
Author

Thank You @alex_colombo for the reply.

I tried with the Qlik trial account/tenant but that account doesn't have the 'Identity Provider' menu.

So recently, I also arranged another Qlik account with premium subscription. Can I create APIs at my own server instead of AWS Lambda platform?

Thanks,

alex_colombo
Employee
Employee

I'm not understanding the question, which API do you want to create? It is something related to Qlik?

pixlics12
Contributor III
Contributor III
Author

I mean, the link you shared for "Embed content using IFrames and anonymous access", have some piece of code which setup on the AWS Lambda function, can I do same code at my own server?

alex_colombo
Employee
Employee

yes you can use whatever tool/language you prefer for generating the JWT tokens.

pixlics12
Contributor III
Contributor III
Author

Thank You @alex_colombo. I go through all the details you shared in this whole conversation and here Embed content using iframes and anonymous access.

-----------------------------------------------------------------------------------

Following is Your reply, sent Yesterday:

Basically, if I understood correctly, you need an anonymous user access, correct? To do so, at the moment, you have to use JWT as IdP, and passing your anonymous user. This will create new users into the tenant. Here you can find a link where it is explained. It is best practice to setting up anonymous access on a tenant should be done on a dedicated tenant.

On Management Console side, below few optional things to do:

Go to Management Console → Configuration → Settings → Feature control. Switch off all except for Creating Groups. If it is already switched off, turn on. This disable some features for anonymous users, it is up to you if this is needed or not.
Under Entitlements section ensure that you also have turned off Enable dynamic assignment of professional users and Enable dynamic assignment of analyzer users. This will not assigned license to anonymous users. As mentioned above, please use a dedicated tenant for anonymous access.

-----------------------------------------------------------------------------------

I find out and conclude that I already followed all the steps, and also have all the things which mentioned in this article (Embed content using iframes and anonymous access) as I also mentioned in my top question.

The code is also quite similar, just some minor differences are there. When I tried steps of this article, I reached at same error which I asked in the top question:

Console error
pixlics12_0-1709227142697.png

Network Log
pixlics12_1-1709228936882.png

-----------------------------------------------------------------------------------
One more thing I noticed today, that the new code is creating an anonymous user every time when I run my code.

pixlics12_2-1709229053699.png

-----------------------------------------------------------------------------------

As I replied that I have arranged another Qlik account with premium subscription, I did setup on same account, and above attached screenshots are for same premium account.
I don't have full access of that account, so below things are remaining to setup as you said:
- Management Console → Configuration → Settings → Feature control/Entitlements

Is this mandatory steps to follow in QMC, or just optional?

Thank You 🙂

alex_colombo
Employee
Employee

Those steps are not mandatory, it depends which kind of features you want to provide to your anonymous users. About the error, I should see it in actions and troubleshoot it, I can't help much than this at this stage. I just want to highlight that where you see a 401 error on users/me API call, this is correct becuase at first time you are not logged in, that's why you received 401.

pixlics12
Contributor III
Contributor III
Author

ok @alex_colombo, Thank you and appreciate your efforts and help.

I will reach out to you, if need something on this.

Thank You very much. 👍🏻

pixlics12
Contributor III
Contributor III
Author

Hello @alex_colombo,

Thank you for all of your help, now finally I am able to embed sheet of an app, using the code which I asked in the question using Enigma.js.

One thing I tried, and it works. I made some changes in the code which generates JWT token. Now I am using following Payload:

 

$payload = array(
	"jti" => guidv4(),
	"sub" => $sub,
	"subType" => "user",
	"name" => 'Qlik Test',
	"email" => 'qliktest@xyz.com',
	"email_verified" => true,
	"exp" => $expir,
	"nbf" => $not_before,
	"iat" => $current_time,
	"aud" => "qlik.api/login/jwt-session",
	"iss" => $tenant,
	"groups" => array('anonymous')
);

 

Prior, I was using the random "name" and "email". But now I am using the "name" and "email" of my user. And I am using same email to login on the Qlik Cloud.And after this change I am able to embed sheet of an app.

----------------------------------------------------------------------------------

But I am facing an issue. I am able to list all of the apps by following API which I am seeing on the Qlik Cloud:

 

GET https://xxxxxxxxxxxxxxx.us.qlikcloud.com/api/v1/items?resourceType=app

 

But I am unable to embed the sheets of all the apps.

I tried to find out the root cause, and got that I can embed the sheets of only app where I am the owner.

pixlics12_0-1710523093826.png

There are more than 300 other apps listed on the Qlik Cloud, and I am unable to embed them. I can view all of the apps and sheets on the Qlik Cloud.

pixlics12_1-1710523954558.png

If I am able to view the app on the Qlik, then it should be embed too.
Can you please help to figure out what I am missing. I think there are some permission related issue. Or only the creator/owner of the app can embed the app.

 

EDIT: One more thing I noticed, all other apps have spaceId, but the app which I owned don't have spaceId,

pixlics12_1-1710526556096.png

Thank You

pixlics12
Contributor III
Contributor III
Author

Also, can you please answer last question and mention all of the issues which we discussed in this conversation in a single message, so that I will mark and accept that as a solution. It will be helpful for other people also.

alex_colombo
Employee
Employee

@pixlics12 try to open the app and check if you are able to see the sheets. Maybe sheets are not published by app owner, that's why you are not able to see them.

About marking the solution as accepted, please pick one and mark it, it is enugh.