Skip to content

Commit 75cffd5

Browse files
Merge pull request #2539 from github/robertbrignull/raw-results-react
Convert RawTable to a function component
2 parents 13a5b78 + 0e033b4 commit 75cffd5

File tree

1 file changed

+136
-136
lines changed

1 file changed

+136
-136
lines changed
Lines changed: 136 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from "react";
2+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23
import {
3-
ResultTableProps,
44
className,
55
emptyQueryResultsMessage,
66
jumpToLocation,
@@ -19,158 +19,158 @@ import { onNavigation } from "./results";
1919
import { tryGetResolvableLocation } from "../../common/bqrs-utils";
2020
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
2121
import { 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

Comments
 (0)