Skip to content

Commit 23b2327

Browse files
Convert AlertTable to a function component
1 parent 450d294 commit 23b2327

File tree

4 files changed

+123
-144
lines changed

4 files changed

+123
-144
lines changed

extensions/ql-vscode/src/view/results/AlertTable.tsx

Lines changed: 117 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,34 @@ import { AlertTableHeader } from "./AlertTableHeader";
2020
import { AlertTableNoResults } from "./AlertTableNoResults";
2121
import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage";
2222
import { AlertTableResultRow } from "./AlertTableResultRow";
23+
import { useEffect, useRef, useState } from "react";
2324

2425
type AlertTableProps = ResultTableProps & {
2526
resultSet: InterpretedResultSet<SarifInterpretationData>;
2627
};
27-
interface AlertTableState {
28-
expanded: Set<string>;
29-
selectedItem: undefined | Keys.ResultKey;
30-
}
31-
32-
export class AlertTable extends React.Component<
33-
AlertTableProps,
34-
AlertTableState
35-
> {
36-
private scroller = new ScrollIntoViewHelper();
3728

38-
constructor(props: AlertTableProps) {
39-
super(props);
40-
this.state = { expanded: new Set<string>(), selectedItem: undefined };
41-
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
29+
export function AlertTable(props: AlertTableProps) {
30+
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
31+
if (scroller.current === undefined) {
32+
scroller.current = new ScrollIntoViewHelper();
4233
}
34+
useEffect(() => scroller.current?.update());
35+
36+
const [expanded, setExpanded] = useState<Set<string>>(new Set<string>());
37+
const [selectedItem, setSelectedItem] = useState<Keys.ResultKey | undefined>(
38+
undefined,
39+
);
4340

4441
/**
4542
* Given a list of `keys`, toggle the first, and if we 'open' the
4643
* first item, open all the rest as well. This mimics vscode's file
4744
* explorer tree view behavior.
4845
*/
49-
toggle(e: React.MouseEvent, keys: Keys.ResultKey[]) {
46+
const toggle = (e: React.MouseEvent, keys: Keys.ResultKey[]) => {
5047
const keyStrings = keys.map(Keys.keyToString);
51-
this.setState((previousState) => {
52-
const expanded = new Set(previousState.expanded);
53-
if (previousState.expanded.has(keyStrings[0])) {
48+
setExpanded((previousExpanded) => {
49+
const expanded = new Set(previousExpanded);
50+
if (previousExpanded.has(keyStrings[0])) {
5451
expanded.delete(keyStrings[0]);
5552
} else {
5653
for (const str of keyStrings) {
@@ -60,16 +57,16 @@ export class AlertTable extends React.Component<
6057
if (expanded) {
6158
sendTelemetry("local-results-alert-table-path-expanded");
6259
}
63-
return { expanded };
60+
return expanded;
6461
});
6562
e.stopPropagation();
6663
e.preventDefault();
67-
}
64+
};
6865

69-
private getNewSelection(
66+
const getNewSelection = (
7067
key: Keys.ResultKey | undefined,
7168
direction: NavigationDirection,
72-
): Keys.ResultKey {
69+
): Keys.ResultKey => {
7370
if (key === undefined) {
7471
return { resultIndex: 0 };
7572
}
@@ -107,129 +104,111 @@ export class AlertTable extends React.Component<
107104
return key;
108105
}
109106
}
110-
}
111-
112-
private handleNavigationEvent(event: NavigateMsg) {
113-
this.setState((prevState) => {
114-
const key = this.getNewSelection(prevState.selectedItem, event.direction);
115-
const data = this.props.resultSet.interpretation.data;
116-
117-
// Check if the selected node actually exists (bounds check) and get its location if relevant
118-
let jumpLocation: Sarif.Location | undefined;
119-
if (key.pathNodeIndex !== undefined) {
120-
jumpLocation = Keys.getPathNode(data, key);
121-
if (jumpLocation === undefined) {
122-
return prevState; // Result does not exist
123-
}
124-
} else if (key.pathIndex !== undefined) {
125-
if (Keys.getPath(data, key) === undefined) {
126-
return prevState; // Path does not exist
127-
}
128-
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
129-
} else {
130-
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
131-
if (jumpLocation === undefined) {
132-
return prevState; // Path step does not exist.
133-
}
107+
};
108+
109+
const handleNavigationEvent = (event: NavigateMsg) => {
110+
const key = getNewSelection(selectedItem, event.direction);
111+
const data = props.resultSet.interpretation.data;
112+
113+
// Check if the selected node actually exists (bounds check) and get its location if relevant
114+
let jumpLocation: Sarif.Location | undefined;
115+
if (key.pathNodeIndex !== undefined) {
116+
jumpLocation = Keys.getPathNode(data, key);
117+
if (jumpLocation === undefined) {
118+
return; // Result does not exist
134119
}
135-
if (jumpLocation !== undefined) {
136-
const parsedLocation = parseSarifLocation(
137-
jumpLocation,
138-
this.props.resultSet.interpretation.sourceLocationPrefix,
139-
);
140-
if (!isNoLocation(parsedLocation)) {
141-
jumpToLocation(parsedLocation, this.props.databaseUri);
142-
}
120+
} else if (key.pathIndex !== undefined) {
121+
if (Keys.getPath(data, key) === undefined) {
122+
return; // Path does not exist
143123
}
144-
145-
const expanded = new Set(prevState.expanded);
146-
if (event.direction === NavigationDirection.right) {
147-
// When stepping right, expand to ensure the selected node is visible
148-
expanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
149-
if (key.pathIndex !== undefined) {
150-
expanded.add(
151-
Keys.keyToString({
152-
resultIndex: key.resultIndex,
153-
pathIndex: key.pathIndex,
154-
}),
155-
);
156-
}
157-
} else if (event.direction === NavigationDirection.left) {
158-
// When stepping left, collapse immediately
159-
expanded.delete(Keys.keyToString(key));
160-
} else {
161-
// When stepping up or down, collapse the previous node
162-
if (prevState.selectedItem !== undefined) {
163-
expanded.delete(Keys.keyToString(prevState.selectedItem));
164-
}
124+
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
125+
} else {
126+
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
127+
if (jumpLocation === undefined) {
128+
return; // Path step does not exist.
165129
}
166-
this.scroller.scrollIntoViewOnNextUpdate();
167-
return {
168-
...prevState,
169-
expanded,
170-
selectedItem: key,
171-
};
172-
});
173-
}
174-
175-
componentDidUpdate() {
176-
this.scroller.update();
177-
}
178-
179-
componentDidMount() {
180-
this.scroller.update();
181-
onNavigation.addListener(this.handleNavigationEvent);
182-
}
183-
184-
componentWillUnmount() {
185-
onNavigation.removeListener(this.handleNavigationEvent);
186-
}
130+
}
131+
if (jumpLocation !== undefined) {
132+
const parsedLocation = parseSarifLocation(
133+
jumpLocation,
134+
props.resultSet.interpretation.sourceLocationPrefix,
135+
);
136+
if (!isNoLocation(parsedLocation)) {
137+
jumpToLocation(parsedLocation, props.databaseUri);
138+
}
139+
}
187140

188-
render(): JSX.Element {
189-
const { databaseUri, resultSet } = this.props;
141+
const newExpanded = new Set(expanded);
142+
if (event.direction === NavigationDirection.right) {
143+
// When stepping right, expand to ensure the selected node is visible
144+
newExpanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
145+
if (key.pathIndex !== undefined) {
146+
newExpanded.add(
147+
Keys.keyToString({
148+
resultIndex: key.resultIndex,
149+
pathIndex: key.pathIndex,
150+
}),
151+
);
152+
}
153+
} else if (event.direction === NavigationDirection.left) {
154+
// When stepping left, collapse immediately
155+
newExpanded.delete(Keys.keyToString(key));
156+
} else {
157+
// When stepping up or down, collapse the previous node
158+
if (selectedItem !== undefined) {
159+
newExpanded.delete(Keys.keyToString(selectedItem));
160+
}
161+
}
162+
scroller.current?.scrollIntoViewOnNextUpdate();
163+
setExpanded(newExpanded);
164+
setSelectedItem(key);
165+
};
166+
167+
useEffect(() => {
168+
onNavigation.addListener(handleNavigationEvent);
169+
return () => {
170+
onNavigation.removeListener(handleNavigationEvent);
171+
};
172+
}, []);
190173

191-
const { numTruncatedResults, sourceLocationPrefix } =
192-
resultSet.interpretation;
174+
const { databaseUri, resultSet } = props;
193175

194-
const updateSelectionCallback = (
195-
resultKey: Keys.PathNode | Keys.Result | undefined,
196-
) => {
197-
this.setState((previousState) => ({
198-
...previousState,
199-
selectedItem: resultKey,
200-
}));
201-
sendTelemetry("local-results-alert-table-path-selected");
202-
};
176+
const { numTruncatedResults, sourceLocationPrefix } =
177+
resultSet.interpretation;
203178

204-
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
205-
return <AlertTableNoResults {...this.props} />;
206-
}
179+
const updateSelectionCallback = (
180+
resultKey: Keys.PathNode | Keys.Result | undefined,
181+
) => {
182+
setSelectedItem(resultKey);
183+
sendTelemetry("local-results-alert-table-path-selected");
184+
};
207185

208-
return (
209-
<table className={className}>
210-
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
211-
<tbody>
212-
{resultSet.interpretation.data.runs[0].results.map(
213-
(result, resultIndex) => (
214-
<AlertTableResultRow
215-
key={resultIndex}
216-
result={result}
217-
resultIndex={resultIndex}
218-
expanded={this.state.expanded}
219-
selectedItem={this.state.selectedItem}
220-
databaseUri={databaseUri}
221-
sourceLocationPrefix={sourceLocationPrefix}
222-
updateSelectionCallback={updateSelectionCallback}
223-
toggleExpanded={this.toggle.bind(this)}
224-
scroller={this.scroller}
225-
/>
226-
),
227-
)}
228-
<AlertTableTruncatedMessage
229-
numTruncatedResults={numTruncatedResults}
230-
/>
231-
</tbody>
232-
</table>
233-
);
186+
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
187+
return <AlertTableNoResults {...props} />;
234188
}
189+
190+
return (
191+
<table className={className}>
192+
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
193+
<tbody>
194+
{resultSet.interpretation.data.runs[0].results.map(
195+
(result, resultIndex) => (
196+
<AlertTableResultRow
197+
key={resultIndex}
198+
result={result}
199+
resultIndex={resultIndex}
200+
expanded={expanded}
201+
selectedItem={selectedItem}
202+
databaseUri={databaseUri}
203+
sourceLocationPrefix={sourceLocationPrefix}
204+
updateSelectionCallback={updateSelectionCallback}
205+
toggleExpanded={toggle}
206+
scroller={scroller.current}
207+
/>
208+
),
209+
)}
210+
<AlertTableTruncatedMessage numTruncatedResults={numTruncatedResults} />
211+
</tbody>
212+
</table>
213+
);
235214
}

extensions/ql-vscode/src/view/results/AlertTablePathNodeRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface Props {
1717
updateSelectionCallback: (
1818
resultKey: Keys.PathNode | Keys.Result | undefined,
1919
) => void;
20-
scroller: ScrollIntoViewHelper;
20+
scroller?: ScrollIntoViewHelper;
2121
}
2222

2323
export function AlertTablePathNodeRow(props: Props) {
@@ -51,7 +51,7 @@ export function AlertTablePathNodeRow(props: Props) {
5151
const zebraIndex = resultIndex + stepIndex;
5252
return (
5353
<tr
54-
ref={scroller.ref(isSelected)}
54+
ref={scroller?.ref(isSelected)}
5555
className={isSelected ? "vscode-codeql__selected-path-node" : undefined}
5656
>
5757
<td className="vscode-codeql__icon-cell">

extensions/ql-vscode/src/view/results/AlertTablePathRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface Props {
1919
resultKey: Keys.PathNode | Keys.Result | undefined,
2020
) => void;
2121
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
22-
scroller: ScrollIntoViewHelper;
22+
scroller?: ScrollIntoViewHelper;
2323
}
2424

2525
export function AlertTablePathRow(props: Props) {
@@ -50,7 +50,7 @@ export function AlertTablePathRow(props: Props) {
5050
return (
5151
<>
5252
<tr
53-
ref={scroller.ref(isPathSpecificallySelected)}
53+
ref={scroller?.ref(isPathSpecificallySelected)}
5454
{...selectableZebraStripe(isPathSpecificallySelected, resultIndex)}
5555
>
5656
<td className="vscode-codeql__icon-cell">

extensions/ql-vscode/src/view/results/AlertTableResultRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface Props {
2121
resultKey: Keys.PathNode | Keys.Result | undefined,
2222
) => void;
2323
toggleExpanded: (e: React.MouseEvent, keys: Keys.ResultKey[]) => void;
24-
scroller: ScrollIntoViewHelper;
24+
scroller?: ScrollIntoViewHelper;
2525
}
2626

2727
export function AlertTableResultRow(props: Props) {
@@ -81,7 +81,7 @@ export function AlertTableResultRow(props: Props) {
8181
return (
8282
<>
8383
<tr
84-
ref={scroller.ref(resultRowIsSelected)}
84+
ref={scroller?.ref(resultRowIsSelected)}
8585
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
8686
>
8787
{result.codeFlows === undefined ? (

0 commit comments

Comments
 (0)