11import * as React from "react" ;
2+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
23import {
3- ResultTableProps ,
44 className ,
55 emptyQueryResultsMessage ,
66 jumpToLocation ,
@@ -19,158 +19,158 @@ import { onNavigation } from "./results";
1919import { tryGetResolvableLocation } from "../../common/bqrs-utils" ;
2020import { ScrollIntoViewHelper } from "./scroll-into-view-helper" ;
2121import { sendTelemetry } from "../common/telemetry" ;
22+ import { assertNever } from "../../common/helpers-pure" ;
2223
23- export type RawTableProps = ResultTableProps & {
24+ export type RawTableProps = {
25+ databaseUri : string ;
2426 resultSet : RawTableResultSet ;
2527 sortState ?: RawResultsSortState ;
2628 offset : number ;
2729} ;
2830
29- interface RawTableState {
30- selectedItem ?: { row : number ; column : number } ;
31+ interface TableItem {
32+ readonly row : number ;
33+ readonly column : number ;
3134}
3235
33- export class RawTable extends React . Component < RawTableProps , RawTableState > {
34- private scroller = new ScrollIntoViewHelper ( ) ;
35-
36- constructor ( props : RawTableProps ) {
37- super ( props ) ;
38- this . setSelection = this . setSelection . bind ( this ) ;
39- this . handleNavigationEvent = this . handleNavigationEvent . bind ( this ) ;
40- this . state = { } ;
36+ export function RawTable ( {
37+ databaseUri,
38+ resultSet,
39+ sortState,
40+ offset,
41+ } : RawTableProps ) {
42+ const [ selectedItem , setSelectedItem ] = useState < TableItem | undefined > ( ) ;
43+
44+ const scroller = useRef < ScrollIntoViewHelper | undefined > ( undefined ) ;
45+ if ( scroller . current === undefined ) {
46+ scroller . current = new ScrollIntoViewHelper ( ) ;
4147 }
48+ useEffect ( ( ) => scroller . current ?. update ( ) ) ;
4249
43- private setSelection ( row : number , column : number ) {
44- this . setState ( ( prev ) => ( {
45- ...prev ,
46- selectedItem : { row, column } ,
47- } ) ) ;
50+ const setSelection = useCallback ( ( row : number , column : number ) : void => {
51+ setSelectedItem ( { row, column } ) ;
4852 sendTelemetry ( "local-results-raw-results-table-selected" ) ;
49- }
50-
51- render ( ) : React . ReactNode {
52- const { resultSet, databaseUri } = this . props ;
53-
54- let dataRows = resultSet . rows ;
55- if ( dataRows . length === 0 ) {
56- return emptyQueryResultsMessage ( ) ;
57- }
58-
59- let numTruncatedResults = 0 ;
60- if ( dataRows . length > RAW_RESULTS_LIMIT ) {
61- numTruncatedResults = dataRows . length - RAW_RESULTS_LIMIT ;
62- dataRows = dataRows . slice ( 0 , RAW_RESULTS_LIMIT ) ;
63- }
64-
65- const tableRows = dataRows . map ( ( row : ResultRow , rowIndex : number ) => (
66- < RawTableRow
67- key = { rowIndex }
68- rowIndex = { rowIndex + this . props . offset }
69- row = { row }
70- databaseUri = { databaseUri }
71- selectedColumn = {
72- this . state . selectedItem ?. row === rowIndex
73- ? this . state . selectedItem ?. column
74- : undefined
53+ } , [ ] ) ;
54+
55+ const navigateWithDelta = useCallback (
56+ ( rowDelta : number , columnDelta : number ) : void => {
57+ setSelectedItem ( ( prevSelectedItem ) => {
58+ const numberOfAlerts = resultSet . rows . length ;
59+ if ( numberOfAlerts === 0 ) {
60+ return prevSelectedItem ;
7561 }
76- onSelected = { this . setSelection }
77- scroller = { this . scroller }
78- />
79- ) ) ;
80-
81- if ( numTruncatedResults > 0 ) {
82- const colSpan = dataRows [ 0 ] . length + 1 ; // one row for each data column, plus index column
83- tableRows . push (
84- < tr >
85- < td
86- key = { "message" }
87- colSpan = { colSpan }
88- style = { { textAlign : "center" , fontStyle : "italic" } }
89- >
90- Too many results to show at once. { numTruncatedResults } result(s)
91- omitted.
92- </ td >
93- </ tr > ,
94- ) ;
95- }
96-
97- return (
98- < table className = { className } >
99- < RawTableHeader
100- columns = { resultSet . schema . columns }
101- schemaName = { resultSet . schema . name }
102- sortState = { this . props . sortState }
103- />
104- < tbody > { tableRows } </ tbody >
105- </ table >
106- ) ;
107- }
108-
109- private handleNavigationEvent ( event : NavigateMsg ) {
110- switch ( event . direction ) {
111- case NavigationDirection . up : {
112- this . navigateWithDelta ( - 1 , 0 ) ;
113- break ;
114- }
115- case NavigationDirection . down : {
116- this . navigateWithDelta ( 1 , 0 ) ;
117- break ;
118- }
119- case NavigationDirection . left : {
120- this . navigateWithDelta ( 0 , - 1 ) ;
121- break ;
122- }
123- case NavigationDirection . right : {
124- this . navigateWithDelta ( 0 , 1 ) ;
125- break ;
62+ const currentRow = prevSelectedItem ?. row ;
63+ const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta ;
64+ if ( nextRow < 0 || nextRow >= numberOfAlerts ) {
65+ return prevSelectedItem ;
66+ }
67+ const currentColumn = prevSelectedItem ?. column ;
68+ const nextColumn =
69+ currentColumn === undefined ? 0 : currentColumn + columnDelta ;
70+ // Jump to the location of the new cell
71+ const rowData = resultSet . rows [ nextRow ] ;
72+ if ( nextColumn < 0 || nextColumn >= rowData . length ) {
73+ return prevSelectedItem ;
74+ }
75+ const cellData = rowData [ nextColumn ] ;
76+ if ( cellData != null && typeof cellData === "object" ) {
77+ const location = tryGetResolvableLocation ( cellData . url ) ;
78+ if ( location !== undefined ) {
79+ jumpToLocation ( location , databaseUri ) ;
80+ }
81+ }
82+ scroller . current ?. scrollIntoViewOnNextUpdate ( ) ;
83+ return { row : nextRow , column : nextColumn } ;
84+ } ) ;
85+ } ,
86+ [ databaseUri , resultSet , scroller ] ,
87+ ) ;
88+
89+ const handleNavigationEvent = useCallback (
90+ ( event : NavigateMsg ) => {
91+ switch ( event . direction ) {
92+ case NavigationDirection . up : {
93+ navigateWithDelta ( - 1 , 0 ) ;
94+ break ;
95+ }
96+ case NavigationDirection . down : {
97+ navigateWithDelta ( 1 , 0 ) ;
98+ break ;
99+ }
100+ case NavigationDirection . left : {
101+ navigateWithDelta ( 0 , - 1 ) ;
102+ break ;
103+ }
104+ case NavigationDirection . right : {
105+ navigateWithDelta ( 0 , 1 ) ;
106+ break ;
107+ }
108+ default :
109+ assertNever ( event . direction ) ;
126110 }
111+ } ,
112+ [ navigateWithDelta ] ,
113+ ) ;
114+
115+ useEffect ( ( ) => {
116+ onNavigation . addListener ( handleNavigationEvent ) ;
117+ return ( ) => {
118+ onNavigation . removeListener ( handleNavigationEvent ) ;
119+ } ;
120+ } , [ handleNavigationEvent ] ) ;
121+
122+ const [ dataRows , numTruncatedResults ] = useMemo ( ( ) => {
123+ if ( resultSet . rows . length <= RAW_RESULTS_LIMIT ) {
124+ return [ resultSet . rows , 0 ] ;
127125 }
126+ return [
127+ resultSet . rows . slice ( 0 , RAW_RESULTS_LIMIT ) ,
128+ resultSet . rows . length - RAW_RESULTS_LIMIT ,
129+ ] ;
130+ } , [ resultSet ] ) ;
131+
132+ if ( dataRows . length === 0 ) {
133+ return emptyQueryResultsMessage ( ) ;
128134 }
129135
130- private navigateWithDelta ( rowDelta : number , columnDelta : number ) {
131- this . setState ( ( prevState ) => {
132- const numberOfAlerts = this . props . resultSet . rows . length ;
133- if ( numberOfAlerts === 0 ) {
134- return prevState ;
135- }
136- const currentRow = prevState . selectedItem ?. row ;
137- const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta ;
138- if ( nextRow < 0 || nextRow >= numberOfAlerts ) {
139- return prevState ;
140- }
141- const currentColumn = prevState . selectedItem ?. column ;
142- const nextColumn =
143- currentColumn === undefined ? 0 : currentColumn + columnDelta ;
144- // Jump to the location of the new cell
145- const rowData = this . props . resultSet . rows [ nextRow ] ;
146- if ( nextColumn < 0 || nextColumn >= rowData . length ) {
147- return prevState ;
136+ const tableRows = dataRows . map ( ( row : ResultRow , rowIndex : number ) => (
137+ < RawTableRow
138+ key = { rowIndex }
139+ rowIndex = { rowIndex + offset }
140+ row = { row }
141+ databaseUri = { databaseUri }
142+ selectedColumn = {
143+ selectedItem ?. row === rowIndex ? selectedItem ?. column : undefined
148144 }
149- const cellData = rowData [ nextColumn ] ;
150- if ( cellData != null && typeof cellData === "object" ) {
151- const location = tryGetResolvableLocation ( cellData . url ) ;
152- if ( location !== undefined ) {
153- jumpToLocation ( location , this . props . databaseUri ) ;
154- }
155- }
156- this . scroller . scrollIntoViewOnNextUpdate ( ) ;
157- return {
158- ...prevState ,
159- selectedItem : { row : nextRow , column : nextColumn } ,
160- } ;
161- } ) ;
162- }
163-
164- componentDidUpdate ( ) {
165- this . scroller . update ( ) ;
166- }
167-
168- componentDidMount ( ) {
169- this . scroller . update ( ) ;
170- onNavigation . addListener ( this . handleNavigationEvent ) ;
145+ onSelected = { setSelection }
146+ scroller = { scroller . current }
147+ />
148+ ) ) ;
149+
150+ if ( numTruncatedResults > 0 ) {
151+ const colSpan = dataRows [ 0 ] . length + 1 ; // one row for each data column, plus index column
152+ tableRows . push (
153+ < tr >
154+ < td
155+ key = { "message" }
156+ colSpan = { colSpan }
157+ style = { { textAlign : "center" , fontStyle : "italic" } }
158+ >
159+ Too many results to show at once. { numTruncatedResults } result(s)
160+ omitted.
161+ </ td >
162+ </ tr > ,
163+ ) ;
171164 }
172165
173- componentWillUnmount ( ) {
174- onNavigation . removeListener ( this . handleNavigationEvent ) ;
175- }
166+ return (
167+ < table className = { className } >
168+ < RawTableHeader
169+ columns = { resultSet . schema . columns }
170+ schemaName = { resultSet . schema . name }
171+ sortState = { sortState }
172+ />
173+ < tbody > { tableRows } </ tbody >
174+ </ table >
175+ ) ;
176176}
0 commit comments