@@ -20,37 +20,34 @@ import { AlertTableHeader } from "./AlertTableHeader";
2020import { AlertTableNoResults } from "./AlertTableNoResults" ;
2121import { AlertTableTruncatedMessage } from "./AlertTableTruncatedMessage" ;
2222import { AlertTableResultRow } from "./AlertTableResultRow" ;
23+ import { useEffect , useRef , useState } from "react" ;
2324
2425type 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}
0 commit comments