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 version = 1;
const type = 'template';

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(record, search, recordData = {}) {
  // It's best to bail out early on if the script is used with an unsupported search.
  const rootSearch = await search.getRoot();
  if (await rootSearch.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;

  // Finally, we return the compiled data
  return recordData;
}

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

function currencyString(value) {
  switch (value) {
    case undefined: return loading;
    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.
 *
 * This function will receive the following parameters:
 * - record: DataRecord instance for the record being displayed
 * - search: DataModelEntity instance for the search that prompted the record display
 * - render: A function that will render a React node in the Record Viewer, used for partial updates
 *
 * The returned value is the final React node that will be rendered in the Record View > Overview
 * tab.
 */
async function buildRecordView(record, search, render) {
  const recordData = {};
  const timerId = setInterval(() => render(reactView(recordData)), 200);
  try {
    await fetchRecordData(record, search, recordData);
  } finally {
    clearInterval(timerId); // Clean up automatic updates before quitting
  }

  return reactView(recordData);
}

/**
 * A download function.
 *
 * Similarly to the record view, download functions receive a DataRecord and a DataModelEntity the
 * record is viewed from.
 *
 * The download function will not return anything - it just needs to start a download of the
 * record's data.
 */
async function buildJsonDownload(record, search) {
  const recordData = await fetchRecordData(record, search);
  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 have your template script produce a downloadable report, see downloadable reports.