Merge pull request #2737 from github/robertbrignull/AlertTable-functional

Convert AlertTable to a function component
This commit is contained in:
Robert
2023-08-22 16:59:00 +01:00
committed by GitHub
4 changed files with 133 additions and 150 deletions

View File

@@ -20,37 +20,36 @@ import { AlertTableHeader } from "./AlertTableHeader";
import { AlertTableNoResults } from "./AlertTableNoResults";
import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage";
import { AlertTableResultRow } from "./AlertTableResultRow";
import { useCallback, useEffect, useRef, useState } from "react";
type AlertTableProps = ResultTableProps & {
resultSet: InterpretedResultSet<SarifInterpretationData>;
};
interface AlertTableState {
expanded: Set<string>;
selectedItem: undefined | Keys.ResultKey;
}
export class AlertTable extends React.Component<
AlertTableProps,
AlertTableState
> {
private scroller = new ScrollIntoViewHelper();
export function AlertTable(props: AlertTableProps) {
const { databaseUri, resultSet } = props;
constructor(props: AlertTableProps) {
super(props);
this.state = { expanded: new Set<string>(), selectedItem: undefined };
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
if (scroller.current === undefined) {
scroller.current = new ScrollIntoViewHelper();
}
useEffect(() => scroller.current?.update());
const [expanded, setExpanded] = useState<Set<string>>(new Set<string>());
const [selectedItem, setSelectedItem] = useState<Keys.ResultKey | undefined>(
undefined,
);
/**
* Given a list of `keys`, toggle the first, and if we 'open' the
* first item, open all the rest as well. This mimics vscode's file
* explorer tree view behavior.
*/
toggle(e: React.MouseEvent, keys: Keys.ResultKey[]) {
const toggle = useCallback((e: React.MouseEvent, keys: Keys.ResultKey[]) => {
const keyStrings = keys.map(Keys.keyToString);
this.setState((previousState) => {
const expanded = new Set(previousState.expanded);
if (previousState.expanded.has(keyStrings[0])) {
setExpanded((previousExpanded) => {
const expanded = new Set(previousExpanded);
if (previousExpanded.has(keyStrings[0])) {
expanded.delete(keyStrings[0]);
} else {
for (const str of keyStrings) {
@@ -60,127 +59,16 @@ export class AlertTable extends React.Component<
if (expanded) {
sendTelemetry("local-results-alert-table-path-expanded");
}
return { expanded };
return expanded;
});
e.stopPropagation();
e.preventDefault();
}
}, []);
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation;
const updateSelectionCallback = (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => {
this.setState((previousState) => ({
...previousState,
selectedItem: resultKey,
}));
sendTelemetry("local-results-alert-table-path-selected");
};
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
return <AlertTableNoResults {...this.props} />;
}
return (
<table className={className}>
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
<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>
);
}
private handleNavigationEvent(event: NavigateMsg) {
this.setState((prevState) => {
const key = this.getNewSelection(prevState.selectedItem, event.direction);
const data = this.props.resultSet.interpretation.data;
// Check if the selected node actually exists (bounds check) and get its location if relevant
let jumpLocation: Sarif.Location | undefined;
if (key.pathNodeIndex !== undefined) {
jumpLocation = Keys.getPathNode(data, key);
if (jumpLocation === undefined) {
return prevState; // Result does not exist
}
} else if (key.pathIndex !== undefined) {
if (Keys.getPath(data, key) === undefined) {
return prevState; // Path does not exist
}
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
} else {
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
if (jumpLocation === undefined) {
return prevState; // Path step does not exist.
}
}
if (jumpLocation !== undefined) {
const parsedLocation = parseSarifLocation(
jumpLocation,
this.props.resultSet.interpretation.sourceLocationPrefix,
);
if (!isNoLocation(parsedLocation)) {
jumpToLocation(parsedLocation, this.props.databaseUri);
}
}
const expanded = new Set(prevState.expanded);
if (event.direction === NavigationDirection.right) {
// When stepping right, expand to ensure the selected node is visible
expanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
if (key.pathIndex !== undefined) {
expanded.add(
Keys.keyToString({
resultIndex: key.resultIndex,
pathIndex: key.pathIndex,
}),
);
}
} else if (event.direction === NavigationDirection.left) {
// When stepping left, collapse immediately
expanded.delete(Keys.keyToString(key));
} else {
// When stepping up or down, collapse the previous node
if (prevState.selectedItem !== undefined) {
expanded.delete(Keys.keyToString(prevState.selectedItem));
}
}
this.scroller.scrollIntoViewOnNextUpdate();
return {
...prevState,
expanded,
selectedItem: key,
};
});
}
private getNewSelection(
const getNewSelection = (
key: Keys.ResultKey | undefined,
direction: NavigationDirection,
): Keys.ResultKey {
): Keys.ResultKey => {
if (key === undefined) {
return { resultIndex: 0 };
}
@@ -218,18 +106,113 @@ export class AlertTable extends React.Component<
return key;
}
}
};
const handleNavigationEvent = useCallback(
(event: NavigateMsg) => {
const key = getNewSelection(selectedItem, event.direction);
const data = resultSet.interpretation.data;
// Check if the selected node actually exists (bounds check) and get its location if relevant
let jumpLocation: Sarif.Location | undefined;
if (key.pathNodeIndex !== undefined) {
jumpLocation = Keys.getPathNode(data, key);
if (jumpLocation === undefined) {
return; // Result does not exist
}
} else if (key.pathIndex !== undefined) {
if (Keys.getPath(data, key) === undefined) {
return; // Path does not exist
}
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
} else {
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
if (jumpLocation === undefined) {
return; // Path step does not exist.
}
}
if (jumpLocation !== undefined) {
const parsedLocation = parseSarifLocation(
jumpLocation,
resultSet.interpretation.sourceLocationPrefix,
);
if (!isNoLocation(parsedLocation)) {
jumpToLocation(parsedLocation, databaseUri);
}
}
const newExpanded = new Set(expanded);
if (event.direction === NavigationDirection.right) {
// When stepping right, expand to ensure the selected node is visible
newExpanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
if (key.pathIndex !== undefined) {
newExpanded.add(
Keys.keyToString({
resultIndex: key.resultIndex,
pathIndex: key.pathIndex,
}),
);
}
} else if (event.direction === NavigationDirection.left) {
// When stepping left, collapse immediately
newExpanded.delete(Keys.keyToString(key));
} else {
// When stepping up or down, collapse the previous node
if (selectedItem !== undefined) {
newExpanded.delete(Keys.keyToString(selectedItem));
}
}
scroller.current?.scrollIntoViewOnNextUpdate();
setExpanded(newExpanded);
setSelectedItem(key);
},
[databaseUri, expanded, resultSet, selectedItem],
);
useEffect(() => {
onNavigation.addListener(handleNavigationEvent);
return () => {
onNavigation.removeListener(handleNavigationEvent);
};
}, [handleNavigationEvent]);
const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation;
const updateSelectionCallback = useCallback(
(resultKey: Keys.PathNode | Keys.Result | undefined) => {
setSelectedItem(resultKey);
sendTelemetry("local-results-alert-table-path-selected");
},
[],
);
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
return <AlertTableNoResults {...props} />;
}
componentDidUpdate() {
this.scroller.update();
}
componentDidMount() {
this.scroller.update();
onNavigation.addListener(this.handleNavigationEvent);
}
componentWillUnmount() {
onNavigation.removeListener(this.handleNavigationEvent);
}
return (
<table className={className}>
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
<tbody>
{resultSet.interpretation.data.runs[0].results.map(
(result, resultIndex) => (
<AlertTableResultRow
key={resultIndex}
result={result}
resultIndex={resultIndex}
expanded={expanded}
selectedItem={selectedItem}
databaseUri={databaseUri}
sourceLocationPrefix={sourceLocationPrefix}
updateSelectionCallback={updateSelectionCallback}
toggleExpanded={toggle}
scroller={scroller.current}
/>
),
)}
<AlertTableTruncatedMessage numTruncatedResults={numTruncatedResults} />
</tbody>
</table>
);
}

View File

@@ -17,7 +17,7 @@ interface Props {
updateSelectionCallback: (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
scroller: ScrollIntoViewHelper;
scroller?: ScrollIntoViewHelper;
}
export function AlertTablePathNodeRow(props: Props) {
@@ -51,7 +51,7 @@ export function AlertTablePathNodeRow(props: Props) {
const zebraIndex = resultIndex + stepIndex;
return (
<tr
ref={scroller.ref(isSelected)}
ref={scroller?.ref(isSelected)}
className={isSelected ? "vscode-codeql__selected-path-node" : undefined}
>
<td className="vscode-codeql__icon-cell">

View File

@@ -19,7 +19,7 @@ interface Props {
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
scroller: ScrollIntoViewHelper;
scroller?: ScrollIntoViewHelper;
}
export function AlertTablePathRow(props: Props) {
@@ -50,7 +50,7 @@ export function AlertTablePathRow(props: Props) {
return (
<>
<tr
ref={scroller.ref(isPathSpecificallySelected)}
ref={scroller?.ref(isPathSpecificallySelected)}
{...selectableZebraStripe(isPathSpecificallySelected, resultIndex)}
>
<td className="vscode-codeql__icon-cell">

View File

@@ -21,7 +21,7 @@ interface Props {
resultKey: Keys.PathNode | Keys.Result | undefined,
) => void;
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
scroller: ScrollIntoViewHelper;
scroller?: ScrollIntoViewHelper;
}
export function AlertTableResultRow(props: Props) {
@@ -81,7 +81,7 @@ export function AlertTableResultRow(props: Props) {
return (
<>
<tr
ref={scroller.ref(resultRowIsSelected)}
ref={scroller?.ref(resultRowIsSelected)}
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
>
{result.codeFlows === undefined ? (