Merge pull request #2732 from github/robertbrignull/AlertTable-decompose

Split AlertTable into smaller components
This commit is contained in:
Robert
2023-08-22 12:24:12 +01:00
committed by GitHub
6 changed files with 379 additions and 247 deletions

View File

@@ -1,11 +1,9 @@
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { info, listUnordered } from "./octicons";
import {
className,
ResultTableProps,
selectableZebraStripe,
jumpToLocation,
} from "./result-table-utils";
import { onNavigation } from "./ResultsApp";
@@ -19,11 +17,9 @@ import { parseSarifLocation, isNoLocation } from "../../common/sarif-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { sendTelemetry } from "../common/telemetry";
import { AlertTableHeader } from "./AlertTableHeader";
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
import { SarifLocation } from "./locations/SarifLocation";
import { EmptyQueryResultsMessage } from "./EmptyQueryResultsMessage";
import TextButton from "../common/TextButton";
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
import { AlertTableNoResults } from "./AlertTableNoResults";
import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage";
import { AlertTableResultRow } from "./AlertTableResultRow";
type AlertTableProps = ResultTableProps & {
resultSet: InterpretedResultSet<SarifInterpretationData>;
@@ -70,263 +66,50 @@ export class AlertTable extends React.Component<
e.preventDefault();
}
renderNoResults(): JSX.Element {
if (this.props.nonemptyRawResults) {
return (
<span>
No Alerts. See{" "}
<TextButton onClick={this.props.showRawResults}>
raw results
</TextButton>
.
</span>
);
} else {
return <EmptyQueryResultsMessage />;
}
}
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
const rows: JSX.Element[] = [];
const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation;
const updateSelectionCallback = (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => {
return () => {
this.setState((previousState) => ({
...previousState,
selectedItem: resultKey,
}));
sendTelemetry("local-results-alert-table-path-selected");
};
};
const toggler: (keys: Keys.ResultKey[]) => (e: React.MouseEvent) => void = (
indices,
) => {
return (e) => this.toggle(e, indices);
this.setState((previousState) => ({
...previousState,
selectedItem: resultKey,
}));
sendTelemetry("local-results-alert-table-path-selected");
};
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
return this.renderNoResults();
}
resultSet.interpretation.data.runs[0].results.forEach(
(result, resultIndex) => {
const resultKey: Keys.Result = { resultIndex };
const text = result.message.text || "[no text]";
const msg =
result.relatedLocations === undefined ? (
<span key="0">{text}</span>
) : (
<SarifMessageWithLocations
msg={text}
relatedLocations={result.relatedLocations}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(resultKey)}
/>
);
const currentResultExpanded = this.state.expanded.has(
Keys.keyToString(resultKey),
);
const location = result.locations !== undefined &&
result.locations.length > 0 && (
<SarifLocation
loc={result.locations[0]}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(resultKey)}
/>
);
const locationCells = (
<td className="vscode-codeql__location-cell">{location}</td>
);
const selectedItem = this.state.selectedItem;
const resultRowIsSelected =
selectedItem?.resultIndex === resultIndex &&
selectedItem.pathIndex === undefined;
if (result.codeFlows === undefined) {
rows.push(
<tr
ref={this.scroller.ref(resultRowIsSelected)}
key={resultIndex}
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
>
<td className="vscode-codeql__icon-cell">{info}</td>
<td colSpan={3}>{msg}</td>
{locationCells}
</tr>,
);
} else {
const paths: Sarif.ThreadFlow[] = Keys.getAllPaths(result);
const indices =
paths.length === 1
? [resultKey, { ...resultKey, pathIndex: 0 }]
: /* if there's exactly one path, auto-expand
* the path when expanding the result */
[resultKey];
rows.push(
<tr
ref={this.scroller.ref(resultRowIsSelected)}
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
key={resultIndex}
>
<AlertTableDropdownIndicatorCell
expanded={currentResultExpanded}
onClick={toggler(indices)}
/>
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
<td colSpan={2}>{msg}</td>
{locationCells}
</tr>,
);
paths.forEach((path, pathIndex) => {
const pathKey = { resultIndex, pathIndex };
const currentPathExpanded = this.state.expanded.has(
Keys.keyToString(pathKey),
);
if (currentResultExpanded) {
const isPathSpecificallySelected = Keys.equalsNotUndefined(
pathKey,
selectedItem,
);
rows.push(
<tr
ref={this.scroller.ref(isPathSpecificallySelected)}
{...selectableZebraStripe(
isPathSpecificallySelected,
resultIndex,
)}
key={`${resultIndex}-${pathIndex}`}
>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<AlertTableDropdownIndicatorCell
expanded={currentPathExpanded}
onClick={toggler([pathKey])}
/>
<td className="vscode-codeql__text-center" colSpan={3}>
Path
</td>
</tr>,
);
}
if (currentResultExpanded && currentPathExpanded) {
const pathNodes = path.locations;
for (
let pathNodeIndex = 0;
pathNodeIndex < pathNodes.length;
++pathNodeIndex
) {
const pathNodeKey: Keys.PathNode = {
...pathKey,
pathNodeIndex,
};
const step = pathNodes[pathNodeIndex];
const msg =
step.location !== undefined &&
step.location.message !== undefined ? (
<SarifLocation
text={step.location.message.text}
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(pathNodeKey)}
/>
) : (
"[no location]"
);
const additionalMsg =
step.location !== undefined ? (
<SarifLocation
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(pathNodeKey)}
/>
) : (
""
);
const isSelected = Keys.equalsNotUndefined(
this.state.selectedItem,
pathNodeKey,
);
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex;
rows.push(
<tr
ref={this.scroller.ref(isSelected)}
className={
isSelected
? "vscode-codeql__selected-path-node"
: undefined
}
key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}
>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__path-index-cell",
)}
>
{stepIndex}
</td>
<td {...selectableZebraStripe(isSelected, zebraIndex)}>
{msg}{" "}
</td>
<td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__location-cell",
)}
>
{additionalMsg}
</td>
</tr>,
);
}
}
});
}
},
);
if (numTruncatedResults > 0) {
rows.push(
<tr key="truncatd-message">
<td colSpan={5} style={{ textAlign: "center", fontStyle: "italic" }}>
Too many results to show at once. {numTruncatedResults} result(s)
omitted.
</td>
</tr>,
);
return <AlertTableNoResults {...this.props} />;
}
return (
<table className={className}>
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
<tbody>{rows}</tbody>
<tbody>
{resultSet.interpretation.data.runs[0].results.map(
(result, resultIndex) => (
<AlertTableResultRow
key={resultIndex}
result={result}
resultIndex={resultIndex}
expanded={this.state.expanded}
selectedItem={this.state.selectedItem}
databaseUri={databaseUri}
sourceLocationPrefix={sourceLocationPrefix}
updateSelectionCallback={updateSelectionCallback}
toggleExpanded={this.toggle.bind(this)}
scroller={this.scroller}
/>
),
)}
<AlertTableTruncatedMessage
numTruncatedResults={numTruncatedResults}
/>
</tbody>
</table>
);
}

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { EmptyQueryResultsMessage } from "./EmptyQueryResultsMessage";
import TextButton from "../common/TextButton";
interface Props {
nonemptyRawResults: boolean;
showRawResults: () => void;
}
export function AlertTableNoResults(props: Props): JSX.Element {
if (props.nonemptyRawResults) {
return (
<span>
No Alerts. See{" "}
<TextButton onClick={props.showRawResults}>raw results</TextButton>.
</span>
);
} else {
return <EmptyQueryResultsMessage />;
}
}

View File

@@ -0,0 +1,103 @@
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { SarifLocation } from "./locations/SarifLocation";
import { selectableZebraStripe } from "./result-table-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { useCallback, useMemo } from "react";
interface Props {
step: Sarif.ThreadFlowLocation;
pathNodeIndex: number;
pathIndex: number;
resultIndex: number;
selectedItem: undefined | Keys.ResultKey;
databaseUri: string;
sourceLocationPrefix: string;
updateSelectionCallback: (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
scroller: ScrollIntoViewHelper;
}
export function AlertTablePathNodeRow(props: Props) {
const {
step,
pathNodeIndex,
pathIndex,
resultIndex,
selectedItem,
databaseUri,
sourceLocationPrefix,
updateSelectionCallback,
scroller,
} = props;
const pathNodeKey: Keys.PathNode = useMemo(
() => ({
resultIndex,
pathIndex,
pathNodeIndex,
}),
[pathIndex, pathNodeIndex, resultIndex],
);
const handleSarifLocationClicked = useCallback(
() => updateSelectionCallback(pathNodeKey),
[pathNodeKey, updateSelectionCallback],
);
const isSelected = Keys.equalsNotUndefined(selectedItem, pathNodeKey);
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex;
return (
<tr
ref={scroller.ref(isSelected)}
className={isSelected ? "vscode-codeql__selected-path-node" : undefined}
>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__path-index-cell",
)}
>
{stepIndex}
</td>
<td {...selectableZebraStripe(isSelected, zebraIndex)}>
{step.location && step.location.message ? (
<SarifLocation
text={step.location.message.text}
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={handleSarifLocationClicked}
/>
) : (
"[no location]"
)}
</td>
<td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__location-cell",
)}
>
{step.location && (
<SarifLocation
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={handleSarifLocationClicked}
/>
)}
</td>
</tr>
);
}

View File

@@ -0,0 +1,78 @@
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { selectableZebraStripe } from "./result-table-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { AlertTablePathNodeRow } from "./AlertTablePathNodeRow";
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
import { useCallback, useMemo } from "react";
interface Props {
path: Sarif.ThreadFlow;
pathIndex: number;
resultIndex: number;
currentPathExpanded: boolean;
selectedItem: undefined | Keys.ResultKey;
databaseUri: string;
sourceLocationPrefix: string;
updateSelectionCallback: (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
scroller: ScrollIntoViewHelper;
}
export function AlertTablePathRow(props: Props) {
const {
path,
pathIndex,
resultIndex,
currentPathExpanded,
selectedItem,
toggleExpanded,
scroller,
} = props;
const pathKey = useMemo(
() => ({ resultIndex, pathIndex }),
[pathIndex, resultIndex],
);
const handleDropdownClick = useCallback(
(e: React.MouseEvent) => toggleExpanded(e, [pathKey]),
[pathKey, toggleExpanded],
);
const isPathSpecificallySelected = Keys.equalsNotUndefined(
pathKey,
selectedItem,
);
return (
<>
<tr
ref={scroller.ref(isPathSpecificallySelected)}
{...selectableZebraStripe(isPathSpecificallySelected, resultIndex)}
>
<td className="vscode-codeql__icon-cell">
<span className="vscode-codeql__vertical-rule"></span>
</td>
<AlertTableDropdownIndicatorCell
expanded={currentPathExpanded}
onClick={handleDropdownClick}
/>
<td className="vscode-codeql__text-center" colSpan={3}>
Path
</td>
</tr>
{currentPathExpanded &&
path.locations.map((step, pathNodeIndex) => (
<AlertTablePathNodeRow
key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}
{...props}
step={step}
pathNodeIndex={pathNodeIndex}
/>
))}
</>
);
}

View File

@@ -0,0 +1,128 @@
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { info, listUnordered } from "./octicons";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { selectableZebraStripe } from "./result-table-utils";
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
import { useCallback, useMemo } from "react";
import { SarifLocation } from "./locations/SarifLocation";
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
import { AlertTablePathRow } from "./AlertTablePathRow";
interface Props {
result: Sarif.Result;
resultIndex: number;
expanded: Set<string>;
selectedItem: undefined | Keys.ResultKey;
databaseUri: string;
sourceLocationPrefix: string;
updateSelectionCallback: (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
scroller: ScrollIntoViewHelper;
}
export function AlertTableResultRow(props: Props) {
const {
result,
resultIndex,
expanded,
selectedItem,
databaseUri,
sourceLocationPrefix,
updateSelectionCallback,
toggleExpanded,
scroller,
} = props;
const resultKey: Keys.Result = useMemo(
() => ({ resultIndex }),
[resultIndex],
);
const handleSarifLocationClicked = useCallback(
() => updateSelectionCallback(resultKey),
[resultKey, updateSelectionCallback],
);
const handleDropdownClick = useCallback(
(e: React.MouseEvent) => {
const indices =
Keys.getAllPaths(result).length === 1
? [resultKey, { ...resultKey, pathIndex: 0 }]
: /* if there's exactly one path, auto-expand
* the path when expanding the result */
[resultKey];
toggleExpanded(e, indices);
},
[result, resultKey, toggleExpanded],
);
const resultRowIsSelected =
selectedItem?.resultIndex === resultIndex &&
selectedItem.pathIndex === undefined;
const text = result.message.text || "[no text]";
const msg =
result.relatedLocations === undefined ? (
<span key="0">{text}</span>
) : (
<SarifMessageWithLocations
msg={text}
relatedLocations={result.relatedLocations}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={handleSarifLocationClicked}
/>
);
const currentResultExpanded = expanded.has(Keys.keyToString(resultKey));
return (
<>
<tr
ref={scroller.ref(resultRowIsSelected)}
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
>
{result.codeFlows === undefined ? (
<>
<td className="vscode-codeql__icon-cell">{info}</td>
<td colSpan={3}>{msg}</td>
</>
) : (
<>
<AlertTableDropdownIndicatorCell
expanded={currentResultExpanded}
onClick={handleDropdownClick}
/>
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
<td colSpan={2}>{msg}</td>
</>
)}
<td className="vscode-codeql__location-cell">
{result.locations && result.locations.length > 0 && (
<SarifLocation
loc={result.locations[0]}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={handleSarifLocationClicked}
/>
)}
</td>
</tr>
{currentResultExpanded &&
result.codeFlows &&
Keys.getAllPaths(result).map((path, pathIndex) => (
<AlertTablePathRow
key={`${resultIndex}-${pathIndex}`}
{...props}
path={path}
pathIndex={pathIndex}
currentPathExpanded={expanded.has(
Keys.keyToString({ resultIndex, pathIndex }),
)}
/>
))}
</>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from "react";
interface Props {
numTruncatedResults: number;
}
export function AlertTableTruncatedMessage(props: Props): JSX.Element | null {
if (props.numTruncatedResults === 0) {
return null;
}
return (
<tr key="truncatd-message">
<td colSpan={5} style={{ textAlign: "center", fontStyle: "italic" }}>
Too many results to show at once. {props.numTruncatedResults} result(s)
omitted.
</td>
</tr>
);
}