Skip to content

Commit d777427

Browse files
Merge pull request #2737 from github/robertbrignull/AlertTable-functional
Convert AlertTable to a function component
2 parents 81924af + dcef43c commit d777427

File tree

4 files changed

+128
-145
lines changed

4 files changed

+128
-145
lines changed

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

Lines changed: 122 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,36 @@ import { AlertTableHeader } from "./AlertTableHeader";
2020
import { AlertTableNoResults } from "./AlertTableNoResults";
2121
import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage";
2222
import { AlertTableResultRow } from "./AlertTableResultRow";
23+
import { useCallback, 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-
}
3128

32-
export class AlertTable extends React.Component<
33-
AlertTableProps,
34-
AlertTableState
35-
> {
36-
private scroller = new ScrollIntoViewHelper();
29+
export function AlertTable(props: AlertTableProps) {
30+
const { databaseUri, resultSet } = props;
3731

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

4443
/**
4544
* Given a list of `keys`, toggle the first, and if we 'open' the
4645
* first item, open all the rest as well. This mimics vscode's file
4746
* explorer tree view behavior.
4847
*/
49-
toggle(e: React.MouseEvent, keys: Keys.ResultKey[]) {
48+
const toggle = useCallback((e: React.MouseEvent, keys: Keys.ResultKey[]) => {
5049
const keyStrings = keys.map(Keys.keyToString);
51-
this.setState((previousState) => {
52-
const expanded = new Set(previousState.expanded);
53-
if (previousState.expanded.has(keyStrings[0])) {
50+
setExpanded((previousExpanded) => {
51+
const expanded = new Set(previousExpanded);
52+
if (previousExpanded.has(keyStrings[0])) {
5453
expanded.delete(keyStrings[0]);
5554
} else {
5655
for (const str of keyStrings) {
@@ -60,99 +59,94 @@ export class AlertTable extends React.Component<
6059
if (expanded) {
6160
sendTelemetry("local-results-alert-table-path-expanded");
6261
}
63-
return { expanded };
62+
return expanded;
6463
});
6564
e.stopPropagation();
6665
e.preventDefault();
67-
}
68-
69-
render(): JSX.Element {
70-
const { databaseUri, resultSet } = this.props;
71-
72-
const { numTruncatedResults, sourceLocationPrefix } =
73-
resultSet.interpretation;
74-
75-
const updateSelectionCallback = (
76-
resultKey: Keys.PathNode | Keys.Result | undefined,
77-
) => {
78-
this.setState((previousState) => ({
79-
...previousState,
80-
selectedItem: resultKey,
81-
}));
82-
sendTelemetry("local-results-alert-table-path-selected");
83-
};
66+
}, []);
8467

85-
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
86-
return <AlertTableNoResults {...this.props} />;
68+
const getNewSelection = (
69+
key: Keys.ResultKey | undefined,
70+
direction: NavigationDirection,
71+
): Keys.ResultKey => {
72+
if (key === undefined) {
73+
return { resultIndex: 0 };
8774
}
75+
const { resultIndex, pathIndex, pathNodeIndex } = key;
76+
switch (direction) {
77+
case NavigationDirection.up:
78+
case NavigationDirection.down: {
79+
const delta = direction === NavigationDirection.up ? -1 : 1;
80+
if (key.pathNodeIndex !== undefined) {
81+
return {
82+
resultIndex,
83+
pathIndex: key.pathIndex,
84+
pathNodeIndex: key.pathNodeIndex + delta,
85+
};
86+
} else if (pathIndex !== undefined) {
87+
return { resultIndex, pathIndex: pathIndex + delta };
88+
} else {
89+
return { resultIndex: resultIndex + delta };
90+
}
91+
}
92+
case NavigationDirection.left:
93+
if (key.pathNodeIndex !== undefined) {
94+
return { resultIndex, pathIndex: key.pathIndex };
95+
} else if (pathIndex !== undefined) {
96+
return { resultIndex };
97+
} else {
98+
return key;
99+
}
100+
case NavigationDirection.right:
101+
if (pathIndex === undefined) {
102+
return { resultIndex, pathIndex: 0 };
103+
} else if (pathNodeIndex === undefined) {
104+
return { resultIndex, pathIndex, pathNodeIndex: 0 };
105+
} else {
106+
return key;
107+
}
108+
}
109+
};
88110

89-
return (
90-
<table className={className}>
91-
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
92-
<tbody>
93-
{resultSet.interpretation.data.runs[0].results.map(
94-
(result, resultIndex) => (
95-
<AlertTableResultRow
96-
key={resultIndex}
97-
result={result}
98-
resultIndex={resultIndex}
99-
expanded={this.state.expanded}
100-
selectedItem={this.state.selectedItem}
101-
databaseUri={databaseUri}
102-
sourceLocationPrefix={sourceLocationPrefix}
103-
updateSelectionCallback={updateSelectionCallback}
104-
toggleExpanded={this.toggle.bind(this)}
105-
scroller={this.scroller}
106-
/>
107-
),
108-
)}
109-
<AlertTableTruncatedMessage
110-
numTruncatedResults={numTruncatedResults}
111-
/>
112-
</tbody>
113-
</table>
114-
);
115-
}
116-
117-
private handleNavigationEvent(event: NavigateMsg) {
118-
this.setState((prevState) => {
119-
const key = this.getNewSelection(prevState.selectedItem, event.direction);
120-
const data = this.props.resultSet.interpretation.data;
111+
const handleNavigationEvent = useCallback(
112+
(event: NavigateMsg) => {
113+
const key = getNewSelection(selectedItem, event.direction);
114+
const data = resultSet.interpretation.data;
121115

122116
// Check if the selected node actually exists (bounds check) and get its location if relevant
123117
let jumpLocation: Sarif.Location | undefined;
124118
if (key.pathNodeIndex !== undefined) {
125119
jumpLocation = Keys.getPathNode(data, key);
126120
if (jumpLocation === undefined) {
127-
return prevState; // Result does not exist
121+
return; // Result does not exist
128122
}
129123
} else if (key.pathIndex !== undefined) {
130124
if (Keys.getPath(data, key) === undefined) {
131-
return prevState; // Path does not exist
125+
return; // Path does not exist
132126
}
133127
jumpLocation = undefined; // When selecting a 'path', don't jump anywhere.
134128
} else {
135129
jumpLocation = Keys.getResult(data, key)?.locations?.[0];
136130
if (jumpLocation === undefined) {
137-
return prevState; // Path step does not exist.
131+
return; // Path step does not exist.
138132
}
139133
}
140134
if (jumpLocation !== undefined) {
141135
const parsedLocation = parseSarifLocation(
142136
jumpLocation,
143-
this.props.resultSet.interpretation.sourceLocationPrefix,
137+
resultSet.interpretation.sourceLocationPrefix,
144138
);
145139
if (!isNoLocation(parsedLocation)) {
146-
jumpToLocation(parsedLocation, this.props.databaseUri);
140+
jumpToLocation(parsedLocation, databaseUri);
147141
}
148142
}
149143

150-
const expanded = new Set(prevState.expanded);
144+
const newExpanded = new Set(expanded);
151145
if (event.direction === NavigationDirection.right) {
152146
// When stepping right, expand to ensure the selected node is visible
153-
expanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
147+
newExpanded.add(Keys.keyToString({ resultIndex: key.resultIndex }));
154148
if (key.pathIndex !== undefined) {
155-
expanded.add(
149+
newExpanded.add(
156150
Keys.keyToString({
157151
resultIndex: key.resultIndex,
158152
pathIndex: key.pathIndex,
@@ -161,75 +155,64 @@ export class AlertTable extends React.Component<
161155
}
162156
} else if (event.direction === NavigationDirection.left) {
163157
// When stepping left, collapse immediately
164-
expanded.delete(Keys.keyToString(key));
158+
newExpanded.delete(Keys.keyToString(key));
165159
} else {
166160
// When stepping up or down, collapse the previous node
167-
if (prevState.selectedItem !== undefined) {
168-
expanded.delete(Keys.keyToString(prevState.selectedItem));
161+
if (selectedItem !== undefined) {
162+
newExpanded.delete(Keys.keyToString(selectedItem));
169163
}
170164
}
171-
this.scroller.scrollIntoViewOnNextUpdate();
172-
return {
173-
...prevState,
174-
expanded,
175-
selectedItem: key,
176-
};
177-
});
178-
}
165+
scroller.current?.scrollIntoViewOnNextUpdate();
166+
setExpanded(newExpanded);
167+
setSelectedItem(key);
168+
},
169+
[databaseUri, expanded, resultSet, selectedItem],
170+
);
171+
172+
useEffect(() => {
173+
onNavigation.addListener(handleNavigationEvent);
174+
return () => {
175+
onNavigation.removeListener(handleNavigationEvent);
176+
};
177+
}, [handleNavigationEvent]);
179178

180-
private getNewSelection(
181-
key: Keys.ResultKey | undefined,
182-
direction: NavigationDirection,
183-
): Keys.ResultKey {
184-
if (key === undefined) {
185-
return { resultIndex: 0 };
186-
}
187-
const { resultIndex, pathIndex, pathNodeIndex } = key;
188-
switch (direction) {
189-
case NavigationDirection.up:
190-
case NavigationDirection.down: {
191-
const delta = direction === NavigationDirection.up ? -1 : 1;
192-
if (key.pathNodeIndex !== undefined) {
193-
return {
194-
resultIndex,
195-
pathIndex: key.pathIndex,
196-
pathNodeIndex: key.pathNodeIndex + delta,
197-
};
198-
} else if (pathIndex !== undefined) {
199-
return { resultIndex, pathIndex: pathIndex + delta };
200-
} else {
201-
return { resultIndex: resultIndex + delta };
202-
}
203-
}
204-
case NavigationDirection.left:
205-
if (key.pathNodeIndex !== undefined) {
206-
return { resultIndex, pathIndex: key.pathIndex };
207-
} else if (pathIndex !== undefined) {
208-
return { resultIndex };
209-
} else {
210-
return key;
211-
}
212-
case NavigationDirection.right:
213-
if (pathIndex === undefined) {
214-
return { resultIndex, pathIndex: 0 };
215-
} else if (pathNodeIndex === undefined) {
216-
return { resultIndex, pathIndex, pathNodeIndex: 0 };
217-
} else {
218-
return key;
219-
}
220-
}
221-
}
179+
const { numTruncatedResults, sourceLocationPrefix } =
180+
resultSet.interpretation;
222181

223-
componentDidUpdate() {
224-
this.scroller.update();
225-
}
182+
const updateSelectionCallback = useCallback(
183+
(resultKey: Keys.PathNode | Keys.Result | undefined) => {
184+
setSelectedItem(resultKey);
185+
sendTelemetry("local-results-alert-table-path-selected");
186+
},
187+
[],
188+
);
226189

227-
componentDidMount() {
228-
this.scroller.update();
229-
onNavigation.addListener(this.handleNavigationEvent);
190+
if (!resultSet.interpretation.data.runs?.[0]?.results?.length) {
191+
return <AlertTableNoResults {...props} />;
230192
}
231193

232-
componentWillUnmount() {
233-
onNavigation.removeListener(this.handleNavigationEvent);
234-
}
194+
return (
195+
<table className={className}>
196+
<AlertTableHeader sortState={resultSet.interpretation.data.sortState} />
197+
<tbody>
198+
{resultSet.interpretation.data.runs[0].results.map(
199+
(result, resultIndex) => (
200+
<AlertTableResultRow
201+
key={resultIndex}
202+
result={result}
203+
resultIndex={resultIndex}
204+
expanded={expanded}
205+
selectedItem={selectedItem}
206+
databaseUri={databaseUri}
207+
sourceLocationPrefix={sourceLocationPrefix}
208+
updateSelectionCallback={updateSelectionCallback}
209+
toggleExpanded={toggle}
210+
scroller={scroller.current}
211+
/>
212+
),
213+
)}
214+
<AlertTableTruncatedMessage numTruncatedResults={numTruncatedResults} />
215+
</tbody>
216+
</table>
217+
);
235218
}

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)