Merge pull request #2279 from github/koesie10/fix-result-view-selected

Fix empty result view when switching between queries
This commit is contained in:
Koen Vlaswinkel
2023-04-11 17:33:58 +02:00
committed by GitHub
2 changed files with 182 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react"; import { act, render as reactRender, screen } from "@testing-library/react";
import { ResultsApp } from "../results"; import { ResultsApp } from "../results";
import { import {
Interpretation, Interpretation,
@@ -20,18 +20,20 @@ const exampleSarif = fs.readJSONSync(
describe(ResultsApp.name, () => { describe(ResultsApp.name, () => {
const render = () => reactRender(<ResultsApp />); const render = () => reactRender(<ResultsApp />);
const postMessage = async (msg: IntoResultsViewMsg) => { const postMessage = async (msg: IntoResultsViewMsg) => {
// window.postMessage doesn't set the origin correctly, see await act(async () => {
// https://github.com/jsdom/jsdom/issues/2745 // window.postMessage doesn't set the origin correctly, see
window.dispatchEvent( // https://github.com/jsdom/jsdom/issues/2745
new MessageEvent("message", { window.dispatchEvent(
source: window, new MessageEvent("message", {
origin: window.location.origin, source: window,
data: msg, origin: window.location.origin,
}), data: msg,
); }),
);
// The event is dispatched asynchronously, so we need to wait for it to be handled. // The event is dispatched asynchronously, so we need to wait for it to be handled.
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
});
}; };
it("renders results", async () => { it("renders results", async () => {
@@ -95,6 +97,7 @@ describe(ResultsApp.name, () => {
}, },
}, },
}; };
await postMessage(message); await postMessage(message);
expect( expect(
@@ -117,4 +120,84 @@ describe(ResultsApp.name, () => {
screen.getByText("'x' is assigned a value but never used."), screen.getByText("'x' is assigned a value but never used."),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("renders results when switching between queries with different result set names", async () => {
render();
await postMessage({
t: "setState",
interpretation: undefined,
origResultsPaths: {
resultsPath: "/a/b/c/results.bqrs",
interpretedResultsPath: "/a/b/c/interpretedResults.sarif",
},
resultsPath: "/a/b/c/results.bqrs",
parsedResultSets: {
pageNumber: 0,
pageSize: 200,
numPages: 1,
numInterpretedPages: 0,
resultSet: {
schema: {
name: "#select",
rows: 1,
columns: [{ kind: "s" }],
pagination: { "step-size": 200, offsets: [13] },
},
rows: [["foobar1"]],
t: "RawResultSet",
},
resultSetNames: ["#select"],
},
sortedResultsMap: {},
database: {
name: "test-db",
databaseUri: "test-db-uri",
},
shouldKeepOldResultsWhileRendering: false,
metadata: {},
queryName: "empty.ql",
queryPath: "/a/b/c/empty.ql",
});
expect(screen.getByText("foobar1")).toBeInTheDocument();
await postMessage({
t: "setState",
interpretation: undefined,
origResultsPaths: {
resultsPath: "/a/b/c/results.bqrs",
interpretedResultsPath: "/a/b/c/interpretedResults.sarif",
},
resultsPath: "/a/b/c/results.bqrs",
parsedResultSets: {
pageNumber: 0,
pageSize: 200,
numPages: 1,
numInterpretedPages: 0,
resultSet: {
schema: {
name: "#Quick_evaluation_of_expression",
rows: 1,
columns: [{ name: "#expr_result", kind: "s" }],
pagination: { "step-size": 200, offsets: [49] },
},
rows: [["foobar2"]],
t: "RawResultSet",
},
resultSetNames: ["#Quick_evaluation_of_expression"],
},
sortedResultsMap: {},
database: {
name: "test-db",
databaseUri: "test-db-uri",
},
shouldKeepOldResultsWhileRendering: false,
metadata: {},
queryName: "Quick evaluation of empty.ql:1",
queryPath: "/a/b/c/empty.ql",
});
expect(screen.getByText("foobar2")).toBeInTheDocument();
});
}); });

View File

@@ -78,6 +78,52 @@ function renderResultCountString(resultSet: ResultSet): JSX.Element {
); );
} }
function getInterpretedTableName(interpretation: Interpretation): string {
return interpretation.data.t === "GraphInterpretationData"
? GRAPH_TABLE_NAME
: ALERTS_TABLE_NAME;
}
function getResultSetNames(
interpretation: Interpretation | undefined,
parsedResultSets: ParsedResultSets,
): string[] {
return interpretation
? parsedResultSets.resultSetNames.concat([
getInterpretedTableName(interpretation),
])
: parsedResultSets.resultSetNames;
}
function getResultSets(
rawResultSets: readonly ResultSet[],
interpretation: Interpretation | undefined,
): ResultSet[] {
const resultSets: ResultSet[] =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2783 Avoid compilation error for overwriting the t property
rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
if (interpretation !== undefined) {
const tableName = getInterpretedTableName(interpretation);
resultSets.push({
t: "InterpretedResultSet",
// FIXME: The values of version, columns, tupleCount are
// unused stubs because a InterpretedResultSet schema isn't used the
// same way as a RawResultSet. Probably should pull `name` field
// out.
schema: {
name: tableName,
rows: 1,
columns: [],
},
name: tableName,
interpretation,
});
}
return resultSets;
}
/** /**
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a * Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown. * dropdown.
@@ -86,51 +132,13 @@ export class ResultTables extends React.Component<
ResultTablesProps, ResultTablesProps,
ResultTablesState ResultTablesState
> { > {
private getResultSets(): ResultSet[] {
const resultSets: ResultSet[] =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2783
this.props.rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
if (this.props.interpretation !== undefined) {
const tableName = this.getInterpretedTableName();
resultSets.push({
t: "InterpretedResultSet",
// FIXME: The values of version, columns, tupleCount are
// unused stubs because a InterpretedResultSet schema isn't used the
// same way as a RawResultSet. Probably should pull `name` field
// out.
schema: {
name: tableName,
rows: 1,
columns: [],
},
name: tableName,
interpretation: this.props.interpretation,
});
}
return resultSets;
}
private getInterpretedTableName(): string {
return this.props.interpretation?.data.t === "GraphInterpretationData"
? GRAPH_TABLE_NAME
: ALERTS_TABLE_NAME;
}
private getResultSetNames(): string[] {
return this.props.interpretation
? this.props.parsedResultSets.resultSetNames.concat([
this.getInterpretedTableName(),
])
: this.props.parsedResultSets.resultSetNames;
}
constructor(props: ResultTablesProps) { constructor(props: ResultTablesProps) {
super(props); super(props);
const selectedTable = const selectedTable =
props.parsedResultSets.selectedTable || props.parsedResultSets.selectedTable ||
getDefaultResultSet(this.getResultSets()); getDefaultResultSet(
getResultSets(props.rawResultSets, props.interpretation),
);
const selectedPage = `${props.parsedResultSets.pageNumber + 1}`; const selectedPage = `${props.parsedResultSets.pageNumber + 1}`;
this.state = { this.state = {
selectedTable, selectedTable,
@@ -139,6 +147,36 @@ export class ResultTables extends React.Component<
}; };
} }
componentDidUpdate(
prevProps: Readonly<ResultTablesProps>,
prevState: Readonly<ResultTablesState>,
snapshot?: any,
) {
const resultSetExists =
this.props.parsedResultSets.resultSetNames.some(
(v) => this.state.selectedTable === v,
) ||
getResultSets(this.props.rawResultSets, this.props.interpretation).some(
(v) => this.state.selectedTable === v.schema.name,
);
// If the selected result set does not exist, select the default result set.
if (!resultSetExists) {
this.setState((state, props) => {
const selectedTable =
props.parsedResultSets.selectedTable ||
getDefaultResultSet(
getResultSets(props.rawResultSets, props.interpretation),
);
return {
selectedTable,
selectedPage: `${props.parsedResultSets.pageNumber + 1}`,
};
});
}
}
untoggleProblemsView() { untoggleProblemsView() {
this.setState({ this.setState({
problemsViewSelected: false, problemsViewSelected: false,
@@ -303,8 +341,14 @@ export class ResultTables extends React.Component<
render(): React.ReactNode { render(): React.ReactNode {
const { selectedTable } = this.state; const { selectedTable } = this.state;
const resultSets = this.getResultSets(); const resultSets = getResultSets(
const resultSetNames = this.getResultSetNames(); this.props.rawResultSets,
this.props.interpretation,
);
const resultSetNames = getResultSetNames(
this.props.interpretation,
this.props.parsedResultSets,
);
const resultSet = resultSets.find( const resultSet = resultSets.find(
(resultSet) => resultSet.schema.name === selectedTable, (resultSet) => resultSet.schema.name === selectedTable,