Merge pull request #481 from jcreedcmu/jcreed/interpreted-pagination

Allow pagination for interpreted results
This commit is contained in:
jcreedcmu
2020-07-06 07:53:06 -04:00
committed by GitHub
5 changed files with 177 additions and 78 deletions

View File

@@ -1,5 +1,6 @@
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from './bqrs-cli-types'; import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from './bqrs-cli-types';
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs'; import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
import { ResultSet } from './interface-types';
// FIXME: This is a temporary bit of impedance matching to convert // FIXME: This is a temporary bit of impedance matching to convert
// from the types provided by ./bqrs-cli-types, to the types used by // from the types provided by ./bqrs-cli-types, to the types used by
@@ -128,7 +129,8 @@ export interface ExtensionParsedResultSets {
t: 'ExtensionParsed'; t: 'ExtensionParsed';
pageNumber: number; pageNumber: number;
numPages: number; numPages: number;
numInterpretedPages: number;
selectedTable?: string; // when undefined, means 'show default table' selectedTable?: string; // when undefined, means 'show default table'
resultSetNames: string[]; resultSetNames: string[];
resultSet: RawResultSet; resultSet: ResultSet;
} }

View File

@@ -23,11 +23,6 @@ export type PathTableResultSet = {
export type ResultSet = RawTableResultSet | PathTableResultSet; export type ResultSet = RawTableResultSet | PathTableResultSet;
/**
* Only ever show this many results per run in interpreted results.
*/
export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
/** /**
* Only ever show this many rows in a raw result table. * Only ever show this many rows in a raw result table.
*/ */
@@ -38,6 +33,11 @@ export const RAW_RESULTS_LIMIT = 10000;
*/ */
export const RAW_RESULTS_PAGE_SIZE = 100; export const RAW_RESULTS_PAGE_SIZE = 100;
/**
* Show this many rows in an interpreted results table at a time.
*/
export const INTERPRETED_RESULTS_PAGE_SIZE = 100;
export interface DatabaseInfo { export interface DatabaseInfo {
name: string; name: string;
databaseUri: string; databaseUri: string;
@@ -61,6 +61,7 @@ export interface PreviousExecution {
export interface Interpretation { export interface Interpretation {
sourceLocationPrefix: string; sourceLocationPrefix: string;
numTruncatedResults: number; numTruncatedResults: number;
numTotalResults: number;
/** /**
* sortState being undefined means don't sort, just present results in the order * sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file. * they appear in the sarif file.
@@ -113,6 +114,16 @@ export interface SetStateMsg {
parsedResultSets: ParsedResultSets; parsedResultSets: ParsedResultSets;
} }
export interface ShowInterpretedPageMsg {
t: 'showInterpretedPage';
interpretation: Interpretation;
database: DatabaseInfo;
metadata?: QueryMetadata;
pageNumber: number;
numPages: number;
resultSetNames: string[];
}
/** Advance to the next or previous path no in the path viewer */ /** Advance to the next or previous path no in the path viewer */
export interface NavigatePathMsg { export interface NavigatePathMsg {
t: 'navigatePath'; t: 'navigatePath';
@@ -124,6 +135,7 @@ export interface NavigatePathMsg {
export type IntoResultsViewMsg = export type IntoResultsViewMsg =
| ResultsUpdatingMsg | ResultsUpdatingMsg
| SetStateMsg | SetStateMsg
| ShowInterpretedPageMsg
| NavigatePathMsg; | NavigatePathMsg;
export type FromResultsViewMsg = export type FromResultsViewMsg =

View File

@@ -19,7 +19,6 @@ import { assertNever } from './helpers-pure';
import { import {
FromResultsViewMsg, FromResultsViewMsg,
Interpretation, Interpretation,
INTERPRETED_RESULTS_PER_RUN_LIMIT,
IntoResultsViewMsg, IntoResultsViewMsg,
QueryMetadata, QueryMetadata,
ResultsPaths, ResultsPaths,
@@ -28,6 +27,8 @@ import {
InterpretedResultsSortState, InterpretedResultsSortState,
SortDirection, SortDirection,
RAW_RESULTS_PAGE_SIZE, RAW_RESULTS_PAGE_SIZE,
INTERPRETED_RESULTS_PAGE_SIZE,
ALERTS_TABLE_NAME,
} from './interface-types'; } from './interface-types';
import { Logger } from './logging'; import { Logger } from './logging';
import * as messages from './messages'; import * as messages from './messages';
@@ -51,6 +52,7 @@ import {
jumpToLocation, jumpToLocation,
} from './interface-utils'; } from './interface-utils';
import { getDefaultResultSetName } from './interface-types'; import { getDefaultResultSetName } from './interface-types';
import { ResultSetSchema } from './bqrs-cli-types';
/** /**
* interface.ts * interface.ts
@@ -95,6 +97,10 @@ function numPagesOfResultSet(resultSet: RawResultSet): number {
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE); return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
} }
function numInterpretedPages(interpretation: Interpretation | undefined): number {
return Math.ceil((interpretation?.sarif.runs[0].results?.length || 0) / INTERPRETED_RESULTS_PAGE_SIZE);
}
export class InterfaceManager extends DisposableObject { export class InterfaceManager extends DisposableObject {
private _displayedQuery?: CompletedQuery; private _displayedQuery?: CompletedQuery;
private _interpretation?: Interpretation; private _interpretation?: Interpretation;
@@ -244,7 +250,12 @@ export class InterfaceManager extends DisposableObject {
); );
break; break;
case 'changePage': case 'changePage':
await this.showPageOfResults(msg.selectedTable, msg.pageNumber); if (msg.selectedTable === ALERTS_TABLE_NAME) {
await this.showPageOfInterpretedResults(msg.pageNumber);
}
else {
await this.showPageOfRawResults(msg.selectedTable, msg.pageNumber);
}
break; break;
default: default:
assertNever(msg); assertNever(msg);
@@ -283,7 +294,8 @@ export class InterfaceManager extends DisposableObject {
return; return;
} }
const interpretation = await this.interpretResultsInfo( this._interpretation = undefined;
const interpretationPage = await this.interpretResultsInfo(
results.query, results.query,
results.interpretedResultsSortState results.interpretedResultsSortState
); );
@@ -295,7 +307,6 @@ export class InterfaceManager extends DisposableObject {
); );
this._displayedQuery = results; this._displayedQuery = results;
this._interpretation = interpretation;
const panel = this.getPanel(); const panel = this.getPanel();
await this.waitForPanelLoaded(); await this.waitForPanelLoaded();
@@ -325,19 +336,13 @@ export class InterfaceManager extends DisposableObject {
const getParsedResultSets = async (): Promise<ParsedResultSets> => { const getParsedResultSets = async (): Promise<ParsedResultSets> => {
if (EXPERIMENTAL_BQRS_SETTING.getValue()) { if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
const schemas = await this.cliServer.bqrsInfo( const resultSetSchemas = await this.getResultSetSchemas(results);
results.query.resultsPaths.resultsPath, const resultSetNames = resultSetSchemas.map(schema => schema.name);
RAW_RESULTS_PAGE_SIZE
);
const resultSetNames = schemas['result-sets'].map(
(resultSet) => resultSet.name
);
// This may not wind up being the page we actually show, if there are interpreted results, // This may not wind up being the page we actually show, if there are interpreted results,
// but speculatively send it anyway. // but speculatively send it anyway.
const selectedTable = getDefaultResultSetName(resultSetNames); const selectedTable = getDefaultResultSetName(resultSetNames);
const schema = schemas['result-sets'].find( const schema = resultSetSchemas.find(
(resultSet) => resultSet.name == selectedTable (resultSet) => resultSet.name == selectedTable
)!; )!;
if (schema === undefined) { if (schema === undefined) {
@@ -352,12 +357,12 @@ export class InterfaceManager extends DisposableObject {
); );
const adaptedSchema = adaptSchema(schema); const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk); const resultSet = adaptBqrs(adaptedSchema, chunk);
return { return {
t: 'ExtensionParsed', t: 'ExtensionParsed',
pageNumber: 0, pageNumber: 0,
numPages: numPagesOfResultSet(resultSet), numPages: numPagesOfResultSet(resultSet),
resultSet, numInterpretedPages: numInterpretedPages(this._interpretation),
resultSet: { t: 'RawResultSet', ...resultSet },
selectedTable: undefined, selectedTable: undefined,
resultSetNames, resultSetNames,
}; };
@@ -368,7 +373,7 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({ await this.postMessage({
t: 'setState', t: 'setState',
interpretation, interpretation: interpretationPage,
origResultsPaths: results.query.resultsPaths, origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri( resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath results.query.resultsPaths.resultsPath
@@ -381,10 +386,48 @@ export class InterfaceManager extends DisposableObject {
}); });
} }
/**
* Show a page of interpreted results
*/
public async showPageOfInterpretedResults(
pageNumber: number
): Promise<void> {
if (this._displayedQuery === undefined) {
throw new Error('Trying to show interpreted results but displayed query was undefined');
}
if (this._interpretation === undefined) {
throw new Error('Trying to show interpreted results but interpretation was undefined');
}
if (this._interpretation.sarif.runs[0].results === undefined) {
throw new Error('Trying to show interpreted results but results were undefined');
}
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
await this.postMessage({
t: 'showInterpretedPage',
interpretation: this.getPageOfInterpretedResults(pageNumber),
database: this._displayedQuery.database,
metadata: this._displayedQuery.query.metadata,
pageNumber,
resultSetNames,
numPages: numInterpretedPages(this._interpretation),
});
}
private async getResultSetSchemas(results: CompletedQuery): Promise<ResultSetSchema[]> {
const schemas = await this.cliServer.bqrsInfo(
results.query.resultsPaths.resultsPath,
RAW_RESULTS_PAGE_SIZE
);
return schemas['result-sets'];
}
/** /**
* Show a page of raw results from the chosen table. * Show a page of raw results from the chosen table.
*/ */
public async showPageOfResults( public async showPageOfRawResults(
selectedTable: string, selectedTable: string,
pageNumber: number pageNumber: number
): Promise<void> { ): Promise<void> {
@@ -399,16 +442,10 @@ export class InterfaceManager extends DisposableObject {
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v)) (sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
); );
const schemas = await this.cliServer.bqrsInfo( const resultSetSchemas = await this.getResultSetSchemas(results);
results.query.resultsPaths.resultsPath, const resultSetNames = resultSetSchemas.map(schema => schema.name);
RAW_RESULTS_PAGE_SIZE
);
const resultSetNames = schemas['result-sets'].map( const schema = resultSetSchemas.find(
(resultSet) => resultSet.name
);
const schema = schemas['result-sets'].find(
(resultSet) => resultSet.name == selectedTable (resultSet) => resultSet.name == selectedTable
)!; )!;
if (schema === undefined) if (schema === undefined)
@@ -426,8 +463,9 @@ export class InterfaceManager extends DisposableObject {
const parsedResultSets: ParsedResultSets = { const parsedResultSets: ParsedResultSets = {
t: 'ExtensionParsed', t: 'ExtensionParsed',
pageNumber, pageNumber,
resultSet, resultSet: { t: 'RawResultSet', ...resultSet },
numPages: numPagesOfResultSet(resultSet), numPages: numPagesOfResultSet(resultSet),
numInterpretedPages: numInterpretedPages(this._interpretation),
selectedTable: selectedTable, selectedTable: selectedTable,
resultSetNames, resultSetNames,
}; };
@@ -447,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
}); });
} }
private async getTruncatedResults( private async _getInterpretedResults(
metadata: QueryMetadata | undefined, metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths, resultsPaths: ResultsPaths,
sourceInfo: cli.SourceInfo | undefined, sourceInfo: cli.SourceInfo | undefined,
@@ -460,37 +498,58 @@ export class InterfaceManager extends DisposableObject {
resultsPaths, resultsPaths,
sourceInfo sourceInfo
); );
// For performance reasons, limit the number of results we try sarif.runs.forEach(run => {
// to serialize and send to the webview. TODO: possibly also if (run.results !== undefined)
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach((run) => {
if (run.results !== undefined) {
sortInterpretedResults(run.results, sortState); sortInterpretedResults(run.results, sortState);
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults +=
run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
}); });
return {
const numTotalResults = (() => {
if (sarif.runs.length === 0) return 0;
if (sarif.runs[0].results === undefined) return 0;
return sarif.runs[0].results.length;
})();
const interpretation: Interpretation = {
sarif, sarif,
sourceLocationPrefix, sourceLocationPrefix,
numTruncatedResults, numTruncatedResults: 0,
numTotalResults,
sortState, sortState,
}; };
this._interpretation = interpretation;
return interpretation;
}
private getPageOfInterpretedResults(
pageNumber: number
): Interpretation {
function getPageOfRun(run: Sarif.Run): Sarif.Run {
return {
...run, results: run.results?.slice(
INTERPRETED_RESULTS_PAGE_SIZE * pageNumber,
INTERPRETED_RESULTS_PAGE_SIZE * (pageNumber + 1)
)
};
}
if (this._interpretation === undefined) {
throw new Error('Tried to get interpreted results before interpretation finished');
}
if (this._interpretation.sarif.runs.length !== 1) {
this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
}
const interp = this._interpretation;
return {
...interp,
sarif: { ...interp.sarif, runs: [getPageOfRun(interp.sarif.runs[0])] },
};
} }
private async interpretResultsInfo( private async interpretResultsInfo(
query: QueryInfo, query: QueryInfo,
sortState: InterpretedResultsSortState | undefined sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation | undefined> { ): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if ( if (
(await query.canHaveInterpretedResults()) && (await query.canHaveInterpretedResults()) &&
query.quickEvalPosition === undefined // never do results interpretation if quickEval query.quickEvalPosition === undefined // never do results interpretation if quickEval
@@ -507,7 +566,7 @@ export class InterfaceManager extends DisposableObject {
sourceArchive: sourceArchiveUri.fsPath, sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix, sourceLocationPrefix,
}; };
interpretation = await this.getTruncatedResults( await this._getInterpretedResults(
query.metadata, query.metadata,
query.resultsPaths, query.resultsPaths,
sourceInfo, sourceInfo,
@@ -522,7 +581,7 @@ export class InterfaceManager extends DisposableObject {
); );
} }
} }
return interpretation; return this._interpretation && this.getPageOfInterpretedResults(0);
} }
private async showResultsAsDiagnostics( private async showResultsAsDiagnostics(
@@ -541,7 +600,8 @@ export class InterfaceManager extends DisposableObject {
sourceArchive: sourceArchiveUri.fsPath, sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix, sourceLocationPrefix,
}; };
const interpretation = await this.getTruncatedResults( // TODO: Performance-testing to determine whether this truncation is necessary.
const interpretation = await this._getInterpretedResults(
metadata, metadata,
resultsInfo, resultsInfo,
sourceInfo, sourceInfo,

View File

@@ -50,9 +50,7 @@ function getResultCount(resultSet: ResultSet): number {
case 'RawResultSet': case 'RawResultSet':
return resultSet.schema.tupleCount; return resultSet.schema.tupleCount;
case 'SarifResultSet': case 'SarifResultSet':
if (resultSet.sarif.runs.length === 0) return 0; return resultSet.numTotalResults;
if (resultSet.sarif.runs[0].results === undefined) return 0;
return resultSet.sarif.runs[0].results.length + resultSet.numTruncatedResults;
} }
} }
@@ -110,22 +108,11 @@ export class ResultTables
return this.props.parsedResultSets.t === 'ExtensionParsed'; return this.props.parsedResultSets.t === 'ExtensionParsed';
} }
/**
* Holds if we actually should show pagination interface right now. This is
* still false for the time being when we're viewing alerts.
*/
paginationEnabled(): boolean {
return this.paginationAllowed() &&
this.props.parsedResultSets.selectedTable !== ALERTS_TABLE_NAME &&
this.state.selectedTable !== ALERTS_TABLE_NAME;
}
constructor(props: ResultTablesProps) { constructor(props: ResultTablesProps) {
super(props); super(props);
const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets()); const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
let selectedPage: string; let selectedPage: string;
switch (props.parsedResultSets.t) { switch (props.parsedResultSets.t) {
case 'ExtensionParsed': case 'ExtensionParsed':
selectedPage = (props.parsedResultSets.pageNumber + 1) + ''; selectedPage = (props.parsedResultSets.pageNumber + 1) + '';
@@ -134,15 +121,13 @@ export class ResultTables
selectedPage = ''; selectedPage = '';
break; break;
} }
this.state = { selectedTable, selectedPage }; this.state = { selectedTable, selectedPage };
} }
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => { private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const selectedTable = event.target.value; const selectedTable = event.target.value;
const fetchPageFromExtension = this.paginationAllowed() && selectedTable !== ALERTS_TABLE_NAME;
if (fetchPageFromExtension) { if (this.paginationAllowed()) {
vscode.postMessage({ vscode.postMessage({
t: 'changePage', t: 'changePage',
pageNumber: 0, pageNumber: 0,
@@ -189,13 +174,22 @@ export class ResultTables
renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element { renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element {
const selectedTable = this.state.selectedTable; const selectedTable = this.state.selectedTable;
// FIXME: The extension, not the view, should be in charge of deciding whether to initially show
// a raw or alerts page. We have to conditionally recompute the number of pages here, because
// on initial load of query results, resultSets.numPages will have the number of *raw* pages available,
// not interpreted pages, because the extension doesn't know the view will default to showing alerts
// instead.
const numPages = selectedTable == ALERTS_TABLE_NAME ?
resultSets.numInterpretedPages : resultSets.numPages;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ selectedPage: e.target.value }); this.setState({ selectedPage: e.target.value });
}; };
const choosePage = (input: string) => { const choosePage = (input: string) => {
const pageNumber = parseInt(input); const pageNumber = parseInt(input);
if (pageNumber !== undefined && !isNaN(pageNumber)) { if (pageNumber !== undefined && !isNaN(pageNumber)) {
const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, resultSets.numPages - 1)); const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, numPages - 1));
vscode.postMessage({ vscode.postMessage({
t: 'changePage', t: 'changePage',
pageNumber: actualPageNumber, pageNumber: actualPageNumber,
@@ -214,22 +208,24 @@ export class ResultTables
const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
vscode.postMessage({ vscode.postMessage({
t: 'changePage', t: 'changePage',
pageNumber: Math.min(resultSets.pageNumber + 1, resultSets.numPages - 1), pageNumber: Math.min(resultSets.pageNumber + 1, numPages - 1),
selectedTable, selectedTable,
}); });
}; };
return <span> return <span>
<button onClick={prevPage} >&lt;</button> <button onClick={prevPage} >&lt;</button>
<input value={this.state.selectedPage} onChange={onChange} <input size={3} value={this.state.selectedPage} onChange={onChange}
onBlur={e => choosePage(e.target.value)} onBlur={e => choosePage(e.target.value)}
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }} onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
/> />
/ {numPages}
<button value=">" onClick={nextPage} >&gt;</button> <button value=">" onClick={nextPage} >&gt;</button>
</span>; </span>;
} }
renderButtons(): JSX.Element { renderButtons(): JSX.Element {
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationEnabled()) if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationAllowed())
return this.renderPageButtons(this.props.parsedResultSets); return this.renderPageButtons(this.props.parsedResultSets);
else else
return <span />; return <span />;

View File

@@ -17,6 +17,7 @@ import {
NavigatePathMsg, NavigatePathMsg,
QueryMetadata, QueryMetadata,
ResultsPaths, ResultsPaths,
ALERTS_TABLE_NAME,
} from '../interface-types'; } from '../interface-types';
import { EventHandlers as EventHandlerList } from './event-handler-list'; import { EventHandlers as EventHandlerList } from './event-handler-list';
import { ResultTables } from './result-tables'; import { ResultTables } from './result-tables';
@@ -193,6 +194,32 @@ class App extends React.Component<{}, ResultsViewState> {
metadata: msg.metadata, metadata: msg.metadata,
}); });
this.loadResults();
break;
case 'showInterpretedPage':
this.updateStateWithNewResultsInfo({
resultsPath: '', // FIXME: Not used for interpreted, refactor so this is not needed
parsedResultSets: {
t: 'ExtensionParsed',
numPages: msg.numPages,
numInterpretedPages: msg.numPages,
resultSetNames: msg.resultSetNames,
pageNumber: msg.pageNumber,
resultSet: {
t: 'SarifResultSet',
name: ALERTS_TABLE_NAME,
schema: { name: ALERTS_TABLE_NAME, version: 0, columns: [], tupleCount: 1 },
...msg.interpretation,
},
selectedTable: ALERTS_TABLE_NAME,
},
origResultsPaths: undefined as any, // FIXME: Not used for interpreted, refactor so this is not needed
sortedResultsMap: new Map(), // FIXME: Not used for interpreted, refactor so this is not needed
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: true,
metadata: msg.metadata,
});
this.loadResults(); this.loadResults();
break; break;
case 'resultsUpdating': case 'resultsUpdating':
@@ -342,8 +369,10 @@ class App extends React.Component<{}, ResultsViewState> {
displayedResults.resultsInfo !== null displayedResults.resultsInfo !== null
) { ) {
const parsedResultSets = displayedResults.resultsInfo.parsedResultSets; const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
const key = (parsedResultSets.t === 'ExtensionParsed' ? (parsedResultSets.selectedTable || '') + parsedResultSets.pageNumber : '');
return ( return (
<ResultTables <ResultTables
key={key}
parsedResultSets={parsedResultSets} parsedResultSets={parsedResultSets}
rawResultSets={displayedResults.results.resultSets} rawResultSets={displayedResults.results.resultSets}
interpretation={ interpretation={