Asynchronous rendering

A template script might need to fetch many related documents from Elasticsearch in order to render a complete overview, taking several seconds to complete the rendering process.

In order to improve the user experience, you can implement asynchronous rendering, showing partial information on the screen as soon as it is available.

The following version of the script will update the variable that contains the JSON payload of the overview every 200 ms and show a loading indicator until the payload has been fully assembled.

const { DataRecord, DataModelEntity, Relation } = sirenapi;
const { EuiText, EuiTextColor, EuiIcon } = Eui;

const loading = <i>Loading ...</i>;

/**
 * Common function for data fetching, used by all views/downloads.
 */
async function fetchRecordData(input, recordData) {
  const { record, dataModelEntity } = input;

  // It's best to bail out early on if the script is used with an unsupported data model entity.
  const rootEntity = await dataModelEntity.getRoot();
  if (await rootEntity.getLabel() !== 'Companies') {
    throw new Error('This script only works for Company records');
  }

  // We can ask for a label straight away with the getLabel() method.
  recordData.companyName = await record.getLabel();

  // We can fetch linked records using the getLinkedRecords() Record method. We should always supply
  // the maximum number of records to return, plus the sorting order for the returned records.
  const securedInvestmentsRelation = (await sirenapi.Relation.getRelationsByLabel('secured'))[0];
  const investments = await record.getLinkedRecords(securedInvestmentsRelation, {
    size: 1000,
    orderBy: { field: 'raised_amount', order: 'desc' }
  });

  // Now, given our investments we can calculate the total raised amount.
  let raisedAmount = 0;
  for (const investment of investments) {
    const amount = (await investment.getFirstRawFieldValue('raised_amount')) || 0;
    raisedAmount += amount;
  }

  recordData.raisedAmount = raisedAmount;
}

function valueString(value) {
  switch (value) {
    case undefined: return <i>{loading}</i>;
    case null: return <i>No data</i>;
    default: return value;
  }
}

function currencyString(value) {
  switch (value) {
    case undefined: return <i>{loading}</i>;
    case null: return <i>No data</i>;
    case 0: return <i>No investments</i>;
    default: return `${value} $`;
  }
}

/**
 * Explicit data to React transform function, to be used for both partial and final renders.
 */
function reactView(recordData) {
  return (
    <>
      <EuiText>
        <h1>
          <EuiTextColor color="success"><EuiIcon type="cheer" /> {valueString(recordData.companyName)}</EuiTextColor>
        </h1>
        <p>Total raised amount: {currencyString(recordData.raisedAmount)}</p>
      </EuiText>
    </>
  );
}

/**
 * View function for the Record Viewer. It's responsible for:
 *
 *  1. Fetching all necessary data to display for the input record
 *  2. Building a React-based view of the fetched data
 *
 * This input object has the following properties:
 * - record: DataRecord instance for the displayed record
 * - dataModelEntity: DataModelEntity instance for the data model entity that prompted the record display
 * - render: A function that we can use to progressively render the record view
 * - cancelPromise: A promise that gets rejected when execution should be stopped (the user changes record or closes the Record Viewer)
 *
 * The returned value is the final React node rendered in the Record View > Overview tab.
 */
async function buildRecordView(input) {
  const { render } = input;

  const recordData = {};
  const timerId = setInterval(() => render(reactView(recordData)), 200);

  try {
    await fetchRecordData(input, recordData);
  } finally {
    clearInterval(timerId); // Clean up automatic updates before quitting
  }

  return reactView(recordData);
}

/**
 * A download function.
 *
 * Like to the record view, download functions receive an input object with a record and data model entity that
 * prompted the download.
 *
 * The download function will not return anything - it just needs to start a download of the record's data.
 */
async function buildJsonDownload(input) {
  const recordData = {};
  await fetchRecordData(input, recordData);

  const json = JSON.stringify(recordData, null, 2);
  sirenapi.Reporting.downloadString(json, 'data.json', 'application/json');
}

/**
 * Call registerTemplate() to enable the views and downloads available in the script.
 *
 * The "recordView" property declares the view used in the Record Viewer's Overview tab.
 * The "download" property associates a file type to the associated download function.
 */
context.registerTemplate({
  recordView: buildRecordView,
  download: {
    json: buildJsonDownload
  }
});

You should now see the indicator while data is being retrieved:

Loading indicator

As an alternative to plain text you can also show a loading bar using the EuiLoadingContent component, for example:

const { DataRecord, DataModelEntity, Relation } = sirenapi;
const { EuiText, EuiTextColor, EuiIcon, EuiLoadingContent } = Eui;

const loading = <EuiLoadingContent lines={1}/>;

EUI loading indicator

Next steps

To support execution cancellation in your script, see add support for cancellation.