Merge pull request #401 from jcreedcmu/jcreed/pagination

Implement pagination for BQRS results.
This commit is contained in:
jcreedcmu
2020-06-05 10:23:27 -04:00
committed by GitHub
9 changed files with 322 additions and 55 deletions

View File

@@ -101,3 +101,34 @@ export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawRes
rows: page.tuples.map(adaptRow), rows: page.tuples.map(adaptRow),
}; };
} }
/**
* This type has two branches; we are in the process of changing from
* one to the other. The old way is to parse them inside the webview,
* the new way is to parse them in the extension. The main motivation
* for this transition is to make pagination possible in such a way
* that only one page needs to be sent from the extension to the webview.
*/
export type ParsedResultSets = ExtensionParsedResultSets | WebviewParsedResultSets;
/**
* The old method doesn't require any nontrivial information to be included here,
* just a tag to indicate that it is being used.
*/
export interface WebviewParsedResultSets {
t: 'WebviewParsed';
selectedTable?: string; // when undefined, means 'show default table'
}
/**
* The new method includes which bqrs page is being sent, and the
* actual results parsed on the extension side.
*/
export interface ExtensionParsedResultSets {
t: 'ExtensionParsed';
pageNumber: number;
numPages: number;
selectedTable?: string; // when undefined, means 'show default table'
resultSetNames: string[];
resultSet: RawResultSet;
}

View File

@@ -1,6 +1,6 @@
import * as sarif from 'sarif'; import * as sarif from 'sarif';
import { ResolvableLocationValue } from 'semmle-bqrs'; import { ResolvableLocationValue } from 'semmle-bqrs';
import { RawResultSet } from './adapt'; import { ParsedResultSets } from './adapt';
/** /**
* Only ever show this many results per run in interpreted results. * Only ever show this many results per run in interpreted results.
@@ -12,6 +12,11 @@ export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
*/ */
export const RAW_RESULTS_LIMIT = 10000; export const RAW_RESULTS_LIMIT = 10000;
/**
* Show this many rows in a raw result table at a time.
*/
export const RAW_RESULTS_PAGE_SIZE = 100;
export interface DatabaseInfo { export interface DatabaseInfo {
name: string; name: string;
databaseUri: string; databaseUri: string;
@@ -81,9 +86,10 @@ export interface SetStateMsg {
/** /**
* An experimental way of providing results from the extension. * An experimental way of providing results from the extension.
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true. * Should be in the WebviewParsedResultSets branch of the type
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
*/ */
resultSets?: RawResultSet[]; parsedResultSets: ParsedResultSets;
} }
/** Advance to the next or previous path no in the path viewer */ /** Advance to the next or previous path no in the path viewer */
@@ -101,7 +107,8 @@ export type FromResultsViewMsg =
| ToggleDiagnostics | ToggleDiagnostics
| ChangeRawResultsSortMsg | ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg | ChangeInterpretedResultsSortMsg
| ResultViewLoaded; | ResultViewLoaded
| ChangePage;
interface ViewSourceFileMsg { interface ViewSourceFileMsg {
t: 'viewSourceFile'; t: 'viewSourceFile';
@@ -122,6 +129,12 @@ interface ResultViewLoaded {
t: 'resultViewLoaded'; t: 'resultViewLoaded';
} }
interface ChangePage {
t: 'changePage';
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
selectedTable: string;
}
export enum SortDirection { export enum SortDirection {
asc, desc asc, desc
} }

View File

@@ -0,0 +1,22 @@
import { RawResultSet } from "./adapt";
import { ResultSetSchema } from "semmle-bqrs";
import { Interpretation } from "./interface-types";
export const SELECT_TABLE_NAME = '#select';
export const ALERTS_TABLE_NAME = 'alerts';
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
export function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name));
}
export function getDefaultResultSetName(resultSetNames: readonly string[]): string {
// Choose first available result set from the array
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
}

View File

@@ -10,14 +10,15 @@ import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases'; import { DatabaseItem, DatabaseManager } from './databases';
import { showAndLogErrorMessage } from './helpers'; import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure'; import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types'; import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection, RAW_RESULTS_PAGE_SIZE } from './interface-types';
import { Logger } from './logging'; import { Logger } from './logging';
import * as messages from './messages'; import * as messages from './messages';
import { CompletedQuery, interpretResults } from './query-results'; import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries'; import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils'; import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
import { adaptSchema, adaptBqrs, RawResultSet } from './adapt'; import { adaptSchema, adaptBqrs, RawResultSet, ParsedResultSets } from './adapt';
import { EXPERIMENTAL_BQRS_SETTING } from './config'; import { EXPERIMENTAL_BQRS_SETTING } from './config';
import { getDefaultResultSetName } from './interface-utils';
/** /**
* interface.ts * interface.ts
@@ -115,8 +116,13 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
} }
} }
function numPagesOfResultSet(resultSet: RawResultSet): number {
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
}
export class InterfaceManager extends DisposableObject { export class InterfaceManager extends DisposableObject {
private _displayedQuery?: CompletedQuery; private _displayedQuery?: CompletedQuery;
private _interpretation?: Interpretation;
private _panel: vscode.WebviewPanel | undefined; private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false; private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = []; private _panelLoadedCallBacks: (() => void)[] = [];
@@ -288,6 +294,9 @@ export class InterfaceManager extends DisposableObject {
query.updateInterpretedSortState(this.cliServer, msg.sortState) query.updateInterpretedSortState(this.cliServer, msg.sortState)
); );
break; break;
case "changePage":
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
break;
default: default:
assertNever(msg); assertNever(msg);
} }
@@ -339,6 +348,7 @@ 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();
@@ -364,18 +374,37 @@ export class InterfaceManager extends DisposableObject {
}); });
} }
let resultSets: RawResultSet[] | undefined; const getParsedResultSets = async (): Promise<ParsedResultSets> => {
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
if (EXPERIMENTAL_BQRS_SETTING.getValue()) { const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
resultSets = [];
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath); // This may not wind up being the page we actually show, if there are interpreted results,
for (const schema of schemas["result-sets"]) { // but speculatively send it anyway.
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name); const selectedTable = getDefaultResultSetName(resultSetNames);
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
if (schema === undefined) {
return { t: 'WebviewParsed' };
}
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[0]);
const adaptedSchema = adaptSchema(schema); const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk); const resultSet = adaptBqrs(adaptedSchema, chunk);
resultSets.push(resultSet);
return {
t: 'ExtensionParsed',
pageNumber: 0,
numPages: numPagesOfResultSet(resultSet),
resultSet,
selectedTable: undefined,
resultSetNames
};
} }
} else {
return { t: 'WebviewParsed' };
}
};
await this.postMessage({ await this.postMessage({
t: "setState", t: "setState",
@@ -384,7 +413,7 @@ export class InterfaceManager extends DisposableObject {
resultsPath: this.convertPathToWebviewUri( resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath results.query.resultsPaths.resultsPath
), ),
resultSets, parsedResultSets: await getParsedResultSets(),
sortedResultsMap, sortedResultsMap,
database: results.database, database: results.database,
shouldKeepOldResultsWhileRendering, shouldKeepOldResultsWhileRendering,
@@ -392,6 +421,59 @@ export class InterfaceManager extends DisposableObject {
}); });
} }
/**
* Show a page of raw results from the chosen table.
*/
public async showPageOfResults(selectedTable: string, pageNumber: number): Promise<void> {
const results = this._displayedQuery;
if (results === undefined) {
throw new Error('trying to view a page of a query that is not loaded');
}
const sortedResultsMap: SortedResultsMap = {};
results.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
v
))
);
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
if (schema === undefined)
throw new Error(`Query result set '${selectedTable}' not found.`);
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[pageNumber]);
const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk);
const parsedResultSets: ParsedResultSets = {
t: 'ExtensionParsed',
pageNumber,
resultSet,
numPages: numPagesOfResultSet(resultSet),
selectedTable: selectedTable,
resultSetNames
};
await this.postMessage({
t: "setState",
interpretation: this._interpretation,
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
parsedResultSets,
sortedResultsMap,
database: results.database,
shouldKeepOldResultsWhileRendering: false,
metadata: results.query.metadata
});
}
private async getTruncatedResults( private async getTruncatedResults(
metadata: QueryMetadata | undefined, metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths, resultsPaths: ResultsPaths,

View File

@@ -5,9 +5,10 @@ import * as Keys from '../result-keys';
import { LocationStyle } from 'semmle-bqrs'; import { LocationStyle } from 'semmle-bqrs';
import * as octicons from './octicons'; import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils'; import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results'; import { onNavigation, NavigationEvent, vscode } from './results';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils'; import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types'; import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
import { PathTableResultSet } from '../interface-utils';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet }; export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState { export interface PathTableState {

View File

@@ -1,12 +1,14 @@
import * as React from "react"; import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils"; import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, vscode } from "./results"; import { vscode } from "./results";
import { ResultValue } from "../adapt"; import { ResultValue } from "../adapt";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types"; import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
import { RawTableResultSet } from "../interface-utils";
export type RawTableProps = ResultTableProps & { export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet; resultSet: RawTableResultSet;
sortState?: RawResultsSortState; sortState?: RawResultsSortState;
offset: number;
}; };
export class RawTable extends React.Component<RawTableProps, {}> { export class RawTable extends React.Component<RawTableProps, {}> {
@@ -28,7 +30,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
<tr key={rowIndex} {...zebraStripe(rowIndex)}> <tr key={rowIndex} {...zebraStripe(rowIndex)}>
{ {
[ [
<td key={-1}>{rowIndex + 1}</td>, <td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
...row.map((value, columnIndex) => ...row.map((value, columnIndex) =>
<td key={columnIndex}> <td key={columnIndex}>
{ {

View File

@@ -1,8 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs'; import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types'; import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
import { ResultSet, vscode } from './results'; import { vscode } from './results';
import { assertNever } from '../helpers-pure'; import { assertNever } from '../helpers-pure';
import { ResultSet } from '../interface-utils';
export interface ResultTableProps { export interface ResultTableProps {
resultSet: ResultSet; resultSet: ResultSet;
@@ -10,6 +11,7 @@ export interface ResultTableProps {
metadata?: QueryMetadata; metadata?: QueryMetadata;
resultsPath: string | undefined; resultsPath: string | undefined;
sortState?: RawResultsSortState; sortState?: RawResultsSortState;
offset: number;
/** /**
* Holds if there are any raw results. When that is the case, we * Holds if there are any raw results. When that is the case, we

View File

@@ -1,14 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types'; import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState, RAW_RESULTS_PAGE_SIZE } from '../interface-types';
import { PathTable } from './alert-table'; import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table'; import { RawTable } from './raw-results-table';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils'; import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
import { ResultSet, vscode } from './results'; import { vscode } from './results';
import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
import { ResultSet, ALERTS_TABLE_NAME, SELECT_TABLE_NAME, getDefaultResultSet } from '../interface-utils';
/** /**
* Properties for the `ResultTables` component. * Properties for the `ResultTables` component.
*/ */
export interface ResultTablesProps { export interface ResultTablesProps {
parsedResultSets: ParsedResultSets;
rawResultSets: readonly ResultSet[]; rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined; interpretation: Interpretation | undefined;
database: DatabaseInfo; database: DatabaseInfo;
@@ -25,10 +28,9 @@ export interface ResultTablesProps {
*/ */
interface ResultTablesState { interface ResultTablesState {
selectedTable: string; // name of selected result set selectedTable: string; // name of selected result set
selectedPage: string; // stringified selected page
} }
const ALERTS_TABLE_NAME = 'alerts';
const SELECT_TABLE_NAME = '#select';
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text"; const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
function getResultCount(resultSet: ResultSet): number { function getResultCount(resultSet: ResultSet): number {
@@ -75,23 +77,66 @@ export class ResultTables
return resultSets; return resultSets;
} }
private getResultSetNames(resultSets: ResultSet[]): string[] {
if (this.props.parsedResultSets.t === 'ExtensionParsed') {
return this.props.parsedResultSets.resultSetNames.concat([ALERTS_TABLE_NAME]);
}
else {
return resultSets.map(resultSet => resultSet.schema.name);
}
}
/**
* Holds if we have a result set obtained from the extension that came
* from the ExtensionParsed branch of ParsedResultSets. This is evidence
* that the user has the experimental flag turned on that allows extension-side
* bqrs parsing.
*/
paginationAllowed(): boolean {
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);
this.state = { const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
// Get the result set that should be displayed by default
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
};
}
private static getDefaultResultSet(resultSets: readonly ResultSet[]): string { let selectedPage: string;
const resultSetNames = resultSets.map(resultSet => resultSet.schema.name); switch (props.parsedResultSets.t) {
// Choose first available result set from the array case 'ExtensionParsed':
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0]; selectedPage = (props.parsedResultSets.pageNumber + 1) + '';
break;
case 'WebviewParsed':
selectedPage = '';
break;
}
this.state = { selectedTable, selectedPage };
} }
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => { private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({ selectedTable: event.target.value }); const selectedTable = event.target.value;
const fetchPageFromExtension = this.paginationAllowed() && selectedTable !== ALERTS_TABLE_NAME;
if (fetchPageFromExtension) {
vscode.postMessage({
t: 'changePage',
pageNumber: 0,
selectedTable
});
}
else
this.setState({ selectedTable });
} }
private alertTableExtras(): JSX.Element | undefined { private alertTableExtras(): JSX.Element | undefined {
@@ -118,24 +163,81 @@ export class ResultTables
</div>; </div>;
} }
getOffset(): number {
const { parsedResultSets } = this.props;
switch (parsedResultSets.t) {
case 'ExtensionParsed':
return parsedResultSets.pageNumber * RAW_RESULTS_PAGE_SIZE;
case 'WebviewParsed':
return 0;
}
}
renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element {
const selectedTable = this.state.selectedTable;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ selectedPage: e.target.value });
};
const choosePage = (input: string) => {
const pageNumber = parseInt(input);
if (pageNumber !== undefined && !isNaN(pageNumber)) {
const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, resultSets.numPages - 1));
vscode.postMessage({
t: 'changePage',
pageNumber: actualPageNumber,
selectedTable,
});
}
};
const prevPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
vscode.postMessage({
t: 'changePage',
pageNumber: Math.max(resultSets.pageNumber - 1, 0),
selectedTable,
});
};
const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
vscode.postMessage({
t: 'changePage',
pageNumber: Math.min(resultSets.pageNumber + 1, resultSets.numPages - 1),
selectedTable,
});
};
return <span>
<button onClick={prevPage} >&lt;</button>
<input value={this.state.selectedPage} onChange={onChange}
onBlur={e => choosePage(e.target.value)}
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
/>
<button value=">" onClick={nextPage} >&gt;</button>
</span>;
}
renderButtons(): JSX.Element {
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationEnabled())
return this.renderPageButtons(this.props.parsedResultSets);
else
return <span />;
}
render(): React.ReactNode { render(): React.ReactNode {
const { selectedTable } = this.state; const { selectedTable } = this.state;
const resultSets = this.getResultSets(); const resultSets = this.getResultSets();
const resultSetNames = this.getResultSetNames(resultSets);
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable); const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0); const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0);
const numberOfResults = resultSet && renderResultCountString(resultSet); const numberOfResults = resultSet && renderResultCountString(resultSet);
const resultSetOptions =
resultSetNames.map(name => <option key={name} value={name}>{name}</option>);
return <div> return <div>
{this.renderButtons()}
<div className={tableSelectionHeaderClassName}> <div className={tableSelectionHeaderClassName}>
<select value={selectedTable} onChange={this.onTableSelectionChange}> <select value={selectedTable} onChange={this.onTableSelectionChange}>
{ {resultSetOptions}
resultSets.map(resultSet =>
<option key={resultSet.schema.name} value={resultSet.schema.name}>
{resultSet.schema.name}
</option>
)
}
</select> </select>
{numberOfResults} {numberOfResults}
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined} {selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
@@ -152,7 +254,8 @@ export class ResultTables
resultsPath={this.props.resultsPath} resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} sortState={this.props.sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults} nonemptyRawResults={nonemptyRawResults}
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }} /> showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }}
offset={this.getOffset()} />
} }
</div>; </div>;
} }

View File

@@ -1,12 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import * as Rdom from 'react-dom'; import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs'; import * as bqrs from 'semmle-bqrs';
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs'; import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure'; import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types'; import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } 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';
import { RawResultSet, ResultValue, ResultRow } from '../adapt'; import { ResultValue, ResultRow, ParsedResultSets } from '../adapt';
import { ResultSet } from '../interface-utils';
/** /**
* results.tsx * results.tsx
@@ -24,13 +25,6 @@ interface VsCodeApi {
declare const acquireVsCodeApi: () => VsCodeApi; declare const acquireVsCodeApi: () => VsCodeApi;
export const vscode = acquireVsCodeApi(); export const vscode = acquireVsCodeApi();
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> { async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`); throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
@@ -107,8 +101,8 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
} }
interface ResultsInfo { interface ResultsInfo {
parsedResultSets: ParsedResultSets;
resultsPath: string; resultsPath: string;
resultSets: ResultSet[] | undefined;
origResultsPaths: ResultsPaths; origResultsPaths: ResultsPaths;
database: DatabaseInfo; database: DatabaseInfo;
interpretation: Interpretation | undefined; interpretation: Interpretation | undefined;
@@ -169,7 +163,7 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState': case 'setState':
this.updateStateWithNewResultsInfo({ this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath, resultsPath: msg.resultsPath,
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })), parsedResultSets: msg.parsedResultSets,
origResultsPaths: msg.origResultsPaths, origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)), sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database, database: msg.database,
@@ -221,6 +215,16 @@ class App extends React.Component<{}, ResultsViewState> {
}); });
} }
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
const parsedResultSets = resultsInfo.parsedResultSets;
switch (parsedResultSets.t) {
case 'WebviewParsed': return await this.fetchResultSets(resultsInfo);
case 'ExtensionParsed': {
return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }];
}
}
}
private async loadResults(): Promise<void> { private async loadResults(): Promise<void> {
const resultsInfo = this.state.nextResultsInfo; const resultsInfo = this.state.nextResultsInfo;
if (resultsInfo === null) { if (resultsInfo === null) {
@@ -230,7 +234,7 @@ class App extends React.Component<{}, ResultsViewState> {
let results: Results | null = null; let results: Results | null = null;
let statusText = ''; let statusText = '';
try { try {
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo); const resultSets = await this.getResultSets(resultsInfo);
results = { results = {
resultSets, resultSets,
database: resultsInfo.database, database: resultsInfo.database,
@@ -265,7 +269,11 @@ class App extends React.Component<{}, ResultsViewState> {
}); });
} }
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> { /**
* This is deprecated, because it calls `fetch`. We are moving
* towards doing all bqrs parsing in the extension.
*/
private async fetchResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
const unsortedResponse = await fetch(resultsInfo.resultsPath); const unsortedResponse = await fetch(resultsInfo.resultsPath);
const unsortedResultSets = await parseResultSets(unsortedResponse); const unsortedResultSets = await parseResultSets(unsortedResponse);
return Promise.all(unsortedResultSets.map(async unsortedResultSet => { return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
@@ -291,7 +299,10 @@ class App extends React.Component<{}, ResultsViewState> {
render(): JSX.Element { render(): JSX.Element {
const displayedResults = this.state.displayedResults; const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) { if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets} const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
return <ResultTables
parsedResultSets={parsedResultSets}
rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined} interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database} database={displayedResults.results.database}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths} origResultsPaths={displayedResults.resultsInfo.origResultsPaths}