Bookmarks not showing in embedded QuickSight dashboards

Hello,

My team has a web application with QuickSight dashboards embedded in it. I’m trying to add support for Bookmarks per Amazon QuickSight now supports State Persistence and Bookmarks for embedded dashboards, but I haven’t gotten it to work. Below is the code that I believe is relevant for this. Why are bookmarks not displaying? (If I need to expand on any // ... parts, let me know.) Thanks in advance for the help.

1. Set up lambda using boto3

Our lambda_function.py lambda handler uses boto3.QuickSight.Client.generate_embed_url_for_registered_user() to get the embed URL, like so:

import boto3
from botocore.exceptions import ClientError
# ...
response = qs.generate_embed_url_for_registered_user(
  AwsAccountId=self.aws_account_id,
  SessionLifetimeInMinutes=135,
  UserArn=user_arn,
  ExperienceConfiguration={
    'Dashboard': {
      'InitialDashboardId': dashboard_id,
      'FeatureConfigurations': {
        'StatePersistence': {
          'Enabled': True
        },
        'Bookmarks': {
          'Enabled': True
        }
      }
    },
  },
)
# ...
return {
  'statusCode': 200,
  'headers': {"Access-Control-Allow-Origin": "*"},
  'body': json.dumps(response, cls=DateTimeEncoder),
  'isBase64Encoded': bool('false')
}

2. Call API in React code

We have an API Gateway that calls this lambda. In our React code, this API is called in a file named retrieveQSDashboardURL.js like so:

import Axios from "axios";
import { v4 as uuidv4 } from "uuid";
const uuid = uuidv4();
export async function retrieveQSDashboardURL(userEmailAddress, DashboardId, token) {
  try {
    const accessTokenValue = async () => {
      const response = await token;
      const data = await response;
      return data;
    };
    const value = await accessTokenValue();
    const tokenValue = value.accessToken;
    const headers = {
      Authorization: tokenValue,
    };
    const params = {
      dashboard_id: DashboardId,
      correlationId: uuid,
    };
    const quicksightAPIResponse = await new Axios.get(
      process.env.REACT_APP_GET_DASHBOARD_EMBED_URL, // the API url
      {
        headers,
        params,
      }
    );
    return quicksightAPIResponse;
  } catch (error) {
    console.log(error);
  }
}

3. Process and dispatch the API response

The retrieveQSDashboardURL() function is called in a file named retrieveDashboard.js:

import { retrieveQSDashboardURL } from "../../../services/api/aws/retrieveQSDashboardURL";
// ...
const execute = (userEmailAddress, DashboardId, qsDashboardEmbedState, token) =>
  async (dispatch, getState) => {
    try {
      dispatch({ type: RETRIEVE_DASHBOARD_LOADING });
      const quicksightAPIResponse = await retrieveQSDashboardURL(
        userEmailAddress,
        DashboardId,
        token
      );
      EmbedObj = quicksightAPIResponse.data
      EmbedObj.BaseUrl = // edit the base url of quicksightAPIResponse.data for page organizing later
        EmbedObj.EmbedUrl.substr(0, EmbedObj.EmbedUrl.indexOf("/dashboards/")) + "/dashboards/";
      dispatch({
        type: RETRIEVE_DASHBOARD,
        payload: quicksightAPIResponse.data,
      });
    } catch (err) {
      dispatch({ type: RETRIEVE_DASHBOARD_FAILURE, payload: err });
    }
  };
export default execute;

4. Set state data with the API response

This RETRIEVE_DASHBOARD dispatch type is used in a file named dashboardEmbed.js:

import { initialState } from "./initialState";
// ...
const quicksightDashboardEmbed = (
  state = initialState.quicksightDashboardEmbed,
  action
) => {
  const payload = action.payload;

  switch (action.type) {
    case RETRIEVE_DASHBOARD:
      return {
        data: payload,
        loading: false,
        rendered: false,
        rendering: false,
        loaded: true,
        error: null,
        retrieveDateTime: new Date().getTime(),
      };
    // ...other cases...
    default:
      return state;
  }
};
export default quicksightDashboardEmbed;

5. Use that state data to render the embedded QuickSight dashboard (bookmarks not working)

Finally, quicksightDashboardEmbed is used in the file named Dashboard.js, which renders the embedded dashboard in HTML via JSX:

import { useEffect } from "react";
import Grid from "@mui/material/Grid";
import CircularProgress from "@mui/material/CircularProgress";
import { useSelector, useDispatch } from "react-redux";
import { setRenderedTrue, setRenderedFalse } from "../../redux/actions/aws/setRenderDashboard";
import retrieveDashboard from "../../redux/actions/aws/retrieveDashboard";
import Iframe from "react-iframe";
import { useMsal } from "@azure/msal-react";
import { loginRequest } from "../../services/auth/microsoft/authConfig";
import AppBarDashboard from "../common/AppBarDashboard";

function Dashboard(props) {
  const dispatch = useDispatch();
  const { accounts, instance } = useMsal();
  const token = instance.acquireTokenSilent({
    ...loginRequest,
    account: accounts[0],
  });
  const qsDashboardState = props.dashboard;
  const userEmailAddressState = useSelector(
    (state) => state.microsoftUserData.data.userEmailAddress
  );
  const qsDashboardListState = useSelector(
    (state) => state.quicksightDashboards
  );
  const qsDashboardEmbedState = useSelector(
    (state) => state.quicksightDashboardEmbed
  );

  useEffect(() => {
    // ...
    return function cleanup() {
      if (
        qsDashboardEmbedState.loaded &&
        !qsDashboardEmbedState.loading &&
        qsDashboardListState.loaded &&
        !qsDashboardListState.loading
      ) {
        dispatch(setRenderedTrue());
      }
    };
  }, [
    qsDashboardEmbedState,
    qsDashboardState,
    dispatch,
    qsDashboardListState.loaded,
    qsDashboardListState.loading,
    userEmailAddressState,
    token,
  ]);

  return (
    <div data-testid="appDashboard">
      <AppBarDashboard title={qsDashboardState.title} />
      {qsDashboardEmbedState.loading && (
        // ...
      )}
      {qsDashboardEmbedState.loaded && !qsDashboardEmbedState.loading && (
        <>
          {" "}
          <Grid container justifyContent="center" data-testid="dashboardLoaded">
            <Grid item lg={12} md={12} sm={12} xs={12}>
              <Iframe
                url={
                  qsDashboardEmbedState.rendered
                    ? 
                    qsDashboardEmbedState.data.BaseUrl +
                      qsDashboardState.DashboardId
                    : qsDashboardEmbedState.data.EmbedUrl
                }
                width="100%"
                height="1000px"
                id="dashboardContainer"
                className="QuicksightEmbedding"
                display="initial"
                position="relative"
                loading={"LOADING"}
              />
            </Grid>
          </Grid>
        </>
      )}
    </div>
  );
}
export default Dashboard;

How do I get bookmarks to display?

Hi @Drake

Here is the documentation link for QuickSight SDK and has information for enabling Bookmark for registered users.

Please take a look at the steps and sample code available in this link and see it helps resolving issue.

If not, I would recommend filing a case with AWS Support where we can dive into the details so that we can help you further. Here are the steps to open a support case. If your company has someone who manages your AWS account, you might not have direct access to AWS Support and will need to raise an internal ticket to your IT team or whomever manages your AWS account. They should be able to open an AWS Support case on your behalf. Hope this helps!”

1 Like

Hi @Ashok thanks for your reply. This is helpful, though I’m still having troubles getting it to work. Our old implementation of QuickSight embedded dashboards was through the <Iframe> seen in step 5. I’m trying to replace this with the code from the link you sent me, but we’re using React, and it’s my understanding that the <script> tag won’t work in React JSX (see javascript - Adding script tag to React/JSX - Stack Overflow), and I also cannot nest the <body onload="embedDashboard()"> lines inside the React <Grid> tags. Any recommendations?

I was able to get this to work (mostly). Here’s the edits I’ve made to #5 Dashboard.js:

...
import { setRenderedTrue, setRenderedFalse } from "../../redux/actions/aws/setRenderDashboard";
import retrieveDashboard from "../../redux/actions/aws/retrieveDashboard";
// import Iframe from "react-iframe"; // REMOVED
import embedQSDashboard from "../services/embedQSDashboard";  // NEW
import { useMsal } from "@azure/msal-react";
import { loginRequest } from "../../services/auth/microsoft/authConfig";
...
    account: accounts[0],
  });
  const embedDivRef = useRef(null);  // NEW
  const qsDashboardState = props.dashboard;
  const userEmailAddressState = useSelector(
...
          <Grid container justifyContent="center" data-testid="dashboardLoaded">
            <Grid item lg={12} md={12} sm={12} xs={12}>
              <div ref={embedDivRef} id="embedding-container"></div>  {/* NEW */}
              {(() => { // this syntax is for an IIFE (Immediately Invoked Function Expression)  // NEW
                embedQSDashboard(embedDivRef, qsDashboardEmbedState, qsDashboardState)  // NEW
              })()}  // NEW
//            <Iframe ... /> // REMOVED
            </Grid>
          </Grid>
...

I then created file #6 named embedQSDashboard.js to hold the code from your linked resource:

import * as QuickSightEmbedding from "amazon-quicksight-embedding-sdk";

async function embedQSDashboard(embedDivRef, qsDashboardEmbedState, qsDashboardState) {
  const {
    createEmbeddingContext,
  } = QuickSightEmbedding;

  const embeddingContext = await createEmbeddingContext();

  let embed_url = '';
  if (qsDashboardEmbedState.rendered) {
    embed_url = qsDashboardEmbedState.data.BaseUrl + qsDashboardState.DashboardId;
  } else {
    embed_url = qsDashboardEmbedState.data.EmbedUrl;
  }

  const frameOptions = {
    url: embed_url,
    container: embedDivRef.current,
    height: "1000px",
    width: "100%",
    className: "QuicksightEmbedding",
  };

  const contentOptions = {
    toolbarOptions: {
      export: true,
      undoRedo: true,
      reset: true,
      bookmarks: true
    },
  };

  try {
    await embeddingContext.embedDashboard(frameOptions, contentOptions);
//    const embeddedDashboardExperience = await embeddingContext.embedDashboard(frameOptions, contentOptions);
//    // can use embeddedDashboardExperience for Actions (https://github.com/awslabs/amazon-quicksight-embedding-sdk#actions)
  } catch (err) {
    console.error(err);
    // `embeddingContext.embedDashboard()` sometimes causes either "container is
    // required for the experience" or "Cannot read properties of undefined (reading
    // 'eventManager')", but both seem to not impact the ability for the dashboard
    // to work so can likely be ignored
  }
};

export default embedQSDashboard;

I am getting Uncaught Error: Creating the frame timed out at createExperienceFrame.js:204:1 after a minute or two, which I’m still investigating. This doesn’t seem to affect the functionality of the dashboard either, though.

Hello @Drake, were you able to resolve the Uncaught Error issue you were experiencing?

No I was not, though I learned that the error only displays for the user in localhost and is hidden in our Test and Production servers, so I’ve been prioritizing it less. If you have any pointers though, that’s definitely welcome.

Oh, I understand. You may just need to whitelist the localhost url within QuickSight to avoid the error when running locally. If that doesn’t work you can set a dummy url to run when you run the start quicksight command locally, and whitelist that url instead.

Hello @Drake , to allow localhost domain in Quicksight you will need to use the AllowedDomains parameter in the GenerateEmbedUrlForRegisteredUser or GenerateEmbedUrlForAnonymousUser server side APIs to allow it at runtime (for security reasons the QuickSight console doesn’t allow adding localhost domains).

You can find more information about how to allow domains at runtime here.

Hope this helps you to solve the error on the development environment.

My understanding is that you are now able to use bookmarks in your embeded experience in production and test so I have marked this post as solved.

However if you have any further issues please don’t hesitate to contact us.

1 Like

@DylanM and @EnriqueS , whitelisting localhost domain in QuickSight does not resolve the Uncaught Error: Creating the frame timed out at createExperienceFrame.js:204:1 error. And the error also occurs in Test and Prod, it’s just written to the Inspect Console without showing for the user. This isn’t a major issue though, so I’m fine with leaving this question as solved. If I want to investigate this at a later time, I’ll open a new question. Thanks

1 Like