Merge pull request #2702 from github/robertbrignull/ResultTables

Convert ResultTables to a function component
This commit is contained in:
Robert
2023-08-15 09:45:56 +01:00
committed by GitHub
3 changed files with 201 additions and 207 deletions

View File

@@ -0,0 +1,36 @@
import * as React from "react";
import { ALERTS_TABLE_NAME } from "../../common/interface-types";
import {
alertExtrasClassName,
toggleDiagnosticsClassName,
} from "./result-table-utils";
interface Props {
selectedTable: string;
problemsViewSelected: boolean;
handleCheckboxChanged: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export function ProblemsViewCheckbox(props: Props): JSX.Element | null {
const { selectedTable, problemsViewSelected, handleCheckboxChanged } = props;
if (selectedTable !== ALERTS_TABLE_NAME) {
return null;
}
return (
<div className={alertExtrasClassName}>
<div className={toggleDiagnosticsClassName}>
<input
type="checkbox"
id="toggle-diagnostics"
name="toggle-diagnostics"
onChange={handleCheckboxChanged}
checked={problemsViewSelected}
/>
<label htmlFor="toggle-diagnostics">
Show results in Problems view
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import { ResultSet } from "../../common/interface-types";
import { tableHeaderItemClassName } from "./result-table-utils";
interface Props {
resultSet?: ResultSet;
}
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case "RawResultSet":
return resultSet.schema.rows;
case "InterpretedResultSet":
return resultSet.interpretation.numTotalResults;
}
}
export function ResultCount(props: Props): JSX.Element | null {
if (!props.resultSet) {
return null;
}
const resultCount = getResultCount(props.resultSet);
return (
<span className={tableHeaderItemClassName}>
{resultCount} {resultCount === 1 ? "result" : "results"}
</span>
);
}

View File

@@ -14,16 +14,14 @@ import {
ParsedResultSets,
IntoResultsViewMsg,
} from "../../common/interface-types";
import {
tableHeaderClassName,
tableHeaderItemClassName,
toggleDiagnosticsClassName,
alertExtrasClassName,
} from "./result-table-utils";
import { tableHeaderClassName } from "./result-table-utils";
import { vscode } from "../vscode-api";
import { sendTelemetry } from "../common/telemetry";
import { ResultTable } from "./ResultTable";
import { ResultTablesHeader } from "./ResultTablesHeader";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ResultCount } from "./ResultCount";
import { ProblemsViewCheckbox } from "./ProblemsViewCheckbox";
/**
* Properties for the `ResultTables` component.
@@ -43,35 +41,9 @@ interface ResultTablesProps {
queryPath: string;
}
/**
* State for the `ResultTables` component.
*/
interface ResultTablesState {
selectedTable: string; // name of selected result set
problemsViewSelected: boolean;
}
const UPDATING_RESULTS_TEXT_CLASS_NAME =
"vscode-codeql__result-tables-updating-text";
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case "RawResultSet":
return resultSet.schema.rows;
case "InterpretedResultSet":
return resultSet.interpretation.numTotalResults;
}
}
function renderResultCountString(resultSet: ResultSet): JSX.Element {
const resultCount = getResultCount(resultSet);
return (
<span className={tableHeaderItemClassName}>
{resultCount} {resultCount === 1 ? "result" : "results"}
</span>
);
}
function getInterpretedTableName(interpretation: Interpretation): string {
return interpretation.data.t === "GraphInterpretationData"
? GRAPH_TABLE_NAME
@@ -122,72 +94,93 @@ function getResultSets(
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown.
*/
export class ResultTables extends React.Component<
ResultTablesProps,
ResultTablesState
> {
constructor(props: ResultTablesProps) {
super(props);
const selectedTable =
props.parsedResultSets.selectedTable ||
getDefaultResultSet(
getResultSets(props.rawResultSets, props.interpretation),
);
this.state = {
selectedTable,
problemsViewSelected: false,
};
}
export function ResultTables(props: ResultTablesProps) {
const {
parsedResultSets,
rawResultSets,
interpretation,
database,
resultsPath,
metadata,
origResultsPaths,
isLoadingNewResults,
sortStates,
} = props;
componentDidUpdate(
prevProps: Readonly<ResultTablesProps>,
prevState: Readonly<ResultTablesState>,
snapshot?: any,
) {
const [selectedTable, setSelectedTable] = useState(
parsedResultSets.selectedTable ||
getDefaultResultSet(getResultSets(rawResultSets, interpretation)),
);
const [problemsViewSelected, setProblemsViewSelected] = useState(false);
const handleMessage = useCallback((msg: IntoResultsViewMsg): void => {
switch (msg.t) {
case "untoggleShowProblems":
setProblemsViewSelected(false);
break;
default:
// noop
}
}, []);
const vscodeMessageHandler = useCallback(
(evt: MessageEvent): void => {
// sanitize origin
const origin = evt.origin.replace(/\n|\r/g, "");
evt.origin === window.origin
? handleMessage(evt.data as IntoResultsViewMsg)
: console.error(`Invalid event origin ${origin}`);
},
[handleMessage],
);
// TODO: Duplicated from results.tsx consider a way to
// avoid this duplication
useEffect(() => {
window.addEventListener("message", vscodeMessageHandler);
return () => {
window.removeEventListener("message", vscodeMessageHandler);
};
}, [vscodeMessageHandler]);
useEffect(() => {
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,
parsedResultSets.resultSetNames.some((v) => selectedTable === v) ||
getResultSets(rawResultSets, interpretation).some(
(v) => 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 };
});
setSelectedTable(
parsedResultSets.selectedTable ||
getDefaultResultSet(getResultSets(rawResultSets, interpretation)),
);
}
}
}, [parsedResultSets, interpretation, rawResultSets, selectedTable]);
private onTableSelectionChange = (
event: React.ChangeEvent<HTMLSelectElement>,
): void => {
const selectedTable = event.target.value;
vscode.postMessage({
t: "changePage",
pageNumber: 0,
selectedTable,
});
sendTelemetry("local-results-table-selection");
};
const onTableSelectionChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>): void => {
const selectedTable = event.target.value;
vscode.postMessage({
t: "changePage",
pageNumber: 0,
selectedTable,
});
sendTelemetry("local-results-table-selection");
},
[],
);
private alertTableExtras(): JSX.Element | undefined {
const { database, resultsPath, metadata, origResultsPaths } = this.props;
const handleCheckboxChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked === this.state.problemsViewSelected) {
const handleCheckboxChanged = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked === problemsViewSelected) {
// no change
return;
}
this.setState({
problemsViewSelected: e.target.checked,
});
setProblemsViewSelected(e.target.checked);
if (e.target.checked) {
sendTelemetry("local-results-show-results-in-problems-view");
}
@@ -200,133 +193,69 @@ export class ResultTables extends React.Component<
metadata,
});
}
};
},
[database, metadata, origResultsPaths, problemsViewSelected, resultsPath],
);
return (
<div className={alertExtrasClassName}>
<div className={toggleDiagnosticsClassName}>
<input
type="checkbox"
id="toggle-diagnostics"
name="toggle-diagnostics"
onChange={handleCheckboxChanged}
checked={this.state.problemsViewSelected}
/>
<label htmlFor="toggle-diagnostics">
Show results in Problems view
</label>
</div>
</div>
);
}
const offset = parsedResultSets.pageNumber * parsedResultSets.pageSize;
getOffset(): number {
const { parsedResultSets } = this.props;
return parsedResultSets.pageNumber * parsedResultSets.pageSize;
}
const resultSets = useMemo(
() => getResultSets(rawResultSets, interpretation),
[interpretation, rawResultSets],
);
const resultSetNames = getResultSetNames(interpretation, parsedResultSets);
sendResultsPageChangedTelemetry() {
sendTelemetry("local-results-alert-table-page-changed");
}
const resultSet = useMemo(
() =>
resultSets.find((resultSet) => resultSet.schema.name === selectedTable),
[resultSets, selectedTable],
);
const nonemptyRawResults = resultSets.some(
(resultSet) => resultSet.t === "RawResultSet" && resultSet.rows.length > 0,
);
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = getResultSets(
this.props.rawResultSets,
this.props.interpretation,
);
const resultSetNames = getResultSetNames(
this.props.interpretation,
this.props.parsedResultSets,
);
const resultSet = resultSets.find(
(resultSet) => resultSet.schema.name === selectedTable,
);
const nonemptyRawResults = resultSets.some(
(resultSet) =>
resultSet.t === "RawResultSet" && resultSet.rows.length > 0,
);
const numberOfResults = resultSet && renderResultCountString(resultSet);
const resultSetOptions = resultSetNames.map((name) => (
<option key={name} value={name}>
{name}
</option>
));
return (
<div>
<ResultTablesHeader
{...this.props}
selectedTable={this.state.selectedTable}
const resultSetOptions = resultSetNames.map((name) => (
<option key={name} value={name}>
{name}
</option>
));
return (
<div>
<ResultTablesHeader {...props} selectedTable={selectedTable} />
<div className={tableHeaderClassName}></div>
<div className={tableHeaderClassName}>
<select value={selectedTable} onChange={onTableSelectionChange}>
{resultSetOptions}
</select>
<ResultCount resultSet={resultSet} />
<ProblemsViewCheckbox
selectedTable={selectedTable}
problemsViewSelected={problemsViewSelected}
handleCheckboxChanged={handleCheckboxChanged}
/>
<div className={tableHeaderClassName}></div>
<div className={tableHeaderClassName}>
<select value={selectedTable} onChange={this.onTableSelectionChange}>
{resultSetOptions}
</select>
{numberOfResults}
{selectedTable === ALERTS_TABLE_NAME
? this.alertTableExtras()
: undefined}
{this.props.isLoadingNewResults ? (
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>
Updating results
</span>
) : null}
</div>
{resultSet && (
<ResultTable
key={resultSet.schema.name}
resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults}
showRawResults={() => {
this.setState({ selectedTable: SELECT_TABLE_NAME });
sendTelemetry("local-results-show-raw-results");
}}
offset={this.getOffset()}
/>
)}
{isLoadingNewResults ? (
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>
Updating results
</span>
) : null}
</div>
);
}
handleMessage(msg: IntoResultsViewMsg): void {
switch (msg.t) {
case "untoggleShowProblems":
this.setState({
problemsViewSelected: false,
});
break;
default:
// noop
}
}
// TODO: Duplicated from results.tsx consider a way to
// avoid this duplication
componentDidMount(): void {
this.vscodeMessageHandler = this.vscodeMessageHandler.bind(this);
window.addEventListener("message", this.vscodeMessageHandler);
}
componentWillUnmount(): void {
if (this.vscodeMessageHandler) {
window.removeEventListener("message", this.vscodeMessageHandler);
}
}
private vscodeMessageHandler(evt: MessageEvent) {
// sanitize origin
const origin = evt.origin.replace(/\n|\r/g, "");
evt.origin === window.origin
? this.handleMessage(evt.data as IntoResultsViewMsg)
: console.error(`Invalid event origin ${origin}`);
}
{resultSet && (
<ResultTable
key={resultSet.schema.name}
resultSet={resultSet}
databaseUri={database.databaseUri}
resultsPath={resultsPath}
sortState={sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults}
showRawResults={() => {
setSelectedTable(SELECT_TABLE_NAME);
sendTelemetry("local-results-show-raw-results");
}}
offset={offset}
/>
)}
</div>
);
}
function getDefaultResultSet(resultSets: readonly ResultSet[]): string {