diff --git a/extensions/ql-vscode/src/view/results/AlertTable.tsx b/extensions/ql-vscode/src/view/results/AlertTable.tsx index baacdb088cf..8537dc4cdaa 100644 --- a/extensions/ql-vscode/src/view/results/AlertTable.tsx +++ b/extensions/ql-vscode/src/view/results/AlertTable.tsx @@ -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; @@ -70,263 +66,50 @@ export class AlertTable extends React.Component< e.preventDefault(); } - renderNoResults(): JSX.Element { - if (this.props.nonemptyRawResults) { - return ( - - No Alerts. See{" "} - - raw results - - . - - ); - } else { - return ; - } - } - 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 ? ( - {text} - ) : ( - - ); - - const currentResultExpanded = this.state.expanded.has( - Keys.keyToString(resultKey), - ); - const location = result.locations !== undefined && - result.locations.length > 0 && ( - - ); - const locationCells = ( - {location} - ); - - const selectedItem = this.state.selectedItem; - const resultRowIsSelected = - selectedItem?.resultIndex === resultIndex && - selectedItem.pathIndex === undefined; - - if (result.codeFlows === undefined) { - rows.push( - - {info} - {msg} - {locationCells} - , - ); - } 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( - - - {listUnordered} - {msg} - {locationCells} - , - ); - - 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( - - - - - - - Path - - , - ); - } - - 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 ? ( - - ) : ( - "[no location]" - ); - const additionalMsg = - step.location !== undefined ? ( - - ) : ( - "" - ); - const isSelected = Keys.equalsNotUndefined( - this.state.selectedItem, - pathNodeKey, - ); - const stepIndex = pathNodeIndex + 1; // Convert to 1-based - const zebraIndex = resultIndex + stepIndex; - rows.push( - - - - - - - - - {stepIndex} - - - {msg}{" "} - - - {additionalMsg} - - , - ); - } - } - }); - } - }, - ); - - if (numTruncatedResults > 0) { - rows.push( - - - Too many results to show at once. {numTruncatedResults} result(s) - omitted. - - , - ); + return ; } return ( - {rows} + + {resultSet.interpretation.data.runs[0].results.map( + (result, resultIndex) => ( + + ), + )} + +
); } diff --git a/extensions/ql-vscode/src/view/results/AlertTableNoResults.tsx b/extensions/ql-vscode/src/view/results/AlertTableNoResults.tsx new file mode 100644 index 00000000000..e41c02e441b --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTableNoResults.tsx @@ -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 ( + + No Alerts. See{" "} + raw results. + + ); + } else { + return ; + } +} diff --git a/extensions/ql-vscode/src/view/results/AlertTablePathNodeRow.tsx b/extensions/ql-vscode/src/view/results/AlertTablePathNodeRow.tsx new file mode 100644 index 00000000000..759f81b60b8 --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTablePathNodeRow.tsx @@ -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 ( + + + + + + + + + {stepIndex} + + + {step.location && step.location.message ? ( + + ) : ( + "[no location]" + )} + + + {step.location && ( + + )} + + + ); +} diff --git a/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx b/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx new file mode 100644 index 00000000000..2314af3cba5 --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx @@ -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 ( + <> + + + + + + + Path + + + {currentPathExpanded && + path.locations.map((step, pathNodeIndex) => ( + + ))} + + ); +} diff --git a/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx b/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx new file mode 100644 index 00000000000..84cabbd73a8 --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx @@ -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; + 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 ? ( + {text} + ) : ( + + ); + + const currentResultExpanded = expanded.has(Keys.keyToString(resultKey)); + return ( + <> + + {result.codeFlows === undefined ? ( + <> + {info} + {msg} + + ) : ( + <> + + {listUnordered} + {msg} + + )} + + {result.locations && result.locations.length > 0 && ( + + )} + + + {currentResultExpanded && + result.codeFlows && + Keys.getAllPaths(result).map((path, pathIndex) => ( + + ))} + + ); +} diff --git a/extensions/ql-vscode/src/view/results/AlertTableTruncatedMessage.tsx b/extensions/ql-vscode/src/view/results/AlertTableTruncatedMessage.tsx new file mode 100644 index 00000000000..58acb7d7d60 --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTableTruncatedMessage.tsx @@ -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 ( + + + Too many results to show at once. {props.numTruncatedResults} result(s) + omitted. + + + ); +}