Merge pull request #2539 from github/robertbrignull/raw-results-react
Convert RawTable to a function component
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ResultTableProps,
|
||||
className,
|
||||
emptyQueryResultsMessage,
|
||||
jumpToLocation,
|
||||
@@ -19,158 +19,158 @@ import { onNavigation } from "./results";
|
||||
import { tryGetResolvableLocation } from "../../common/bqrs-utils";
|
||||
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
export type RawTableProps = {
|
||||
databaseUri: string;
|
||||
resultSet: RawTableResultSet;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
interface RawTableState {
|
||||
selectedItem?: { row: number; column: number };
|
||||
interface TableItem {
|
||||
readonly row: number;
|
||||
readonly column: number;
|
||||
}
|
||||
|
||||
export class RawTable extends React.Component<RawTableProps, RawTableState> {
|
||||
private scroller = new ScrollIntoViewHelper();
|
||||
export function RawTable({
|
||||
databaseUri,
|
||||
resultSet,
|
||||
sortState,
|
||||
offset,
|
||||
}: RawTableProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<TableItem | undefined>();
|
||||
|
||||
constructor(props: RawTableProps) {
|
||||
super(props);
|
||||
this.setSelection = this.setSelection.bind(this);
|
||||
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
|
||||
this.state = {};
|
||||
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
|
||||
if (scroller.current === undefined) {
|
||||
scroller.current = new ScrollIntoViewHelper();
|
||||
}
|
||||
useEffect(() => scroller.current?.update());
|
||||
|
||||
private setSelection(row: number, column: number) {
|
||||
this.setState((prev) => ({
|
||||
...prev,
|
||||
selectedItem: { row, column },
|
||||
}));
|
||||
const setSelection = useCallback((row: number, column: number): void => {
|
||||
setSelectedItem({ row, column });
|
||||
sendTelemetry("local-results-raw-results-table-selected");
|
||||
}, []);
|
||||
|
||||
const navigateWithDelta = useCallback(
|
||||
(rowDelta: number, columnDelta: number): void => {
|
||||
setSelectedItem((prevSelectedItem) => {
|
||||
const numberOfAlerts = resultSet.rows.length;
|
||||
if (numberOfAlerts === 0) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const currentRow = prevSelectedItem?.row;
|
||||
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
|
||||
if (nextRow < 0 || nextRow >= numberOfAlerts) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const currentColumn = prevSelectedItem?.column;
|
||||
const nextColumn =
|
||||
currentColumn === undefined ? 0 : currentColumn + columnDelta;
|
||||
// Jump to the location of the new cell
|
||||
const rowData = resultSet.rows[nextRow];
|
||||
if (nextColumn < 0 || nextColumn >= rowData.length) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const cellData = rowData[nextColumn];
|
||||
if (cellData != null && typeof cellData === "object") {
|
||||
const location = tryGetResolvableLocation(cellData.url);
|
||||
if (location !== undefined) {
|
||||
jumpToLocation(location, databaseUri);
|
||||
}
|
||||
}
|
||||
scroller.current?.scrollIntoViewOnNextUpdate();
|
||||
return { row: nextRow, column: nextColumn };
|
||||
});
|
||||
},
|
||||
[databaseUri, resultSet, scroller],
|
||||
);
|
||||
|
||||
const handleNavigationEvent = useCallback(
|
||||
(event: NavigateMsg) => {
|
||||
switch (event.direction) {
|
||||
case NavigationDirection.up: {
|
||||
navigateWithDelta(-1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.down: {
|
||||
navigateWithDelta(1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.left: {
|
||||
navigateWithDelta(0, -1);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.right: {
|
||||
navigateWithDelta(0, 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertNever(event.direction);
|
||||
}
|
||||
},
|
||||
[navigateWithDelta],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onNavigation.addListener(handleNavigationEvent);
|
||||
return () => {
|
||||
onNavigation.removeListener(handleNavigationEvent);
|
||||
};
|
||||
}, [handleNavigationEvent]);
|
||||
|
||||
const [dataRows, numTruncatedResults] = useMemo(() => {
|
||||
if (resultSet.rows.length <= RAW_RESULTS_LIMIT) {
|
||||
return [resultSet.rows, 0];
|
||||
}
|
||||
return [
|
||||
resultSet.rows.slice(0, RAW_RESULTS_LIMIT),
|
||||
resultSet.rows.length - RAW_RESULTS_LIMIT,
|
||||
];
|
||||
}, [resultSet]);
|
||||
|
||||
if (dataRows.length === 0) {
|
||||
return emptyQueryResultsMessage();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { resultSet, databaseUri } = this.props;
|
||||
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex + offset}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
selectedColumn={
|
||||
selectedItem?.row === rowIndex ? selectedItem?.column : undefined
|
||||
}
|
||||
onSelected={setSelection}
|
||||
scroller={scroller.current}
|
||||
/>
|
||||
));
|
||||
|
||||
let dataRows = resultSet.rows;
|
||||
if (dataRows.length === 0) {
|
||||
return emptyQueryResultsMessage();
|
||||
}
|
||||
|
||||
let numTruncatedResults = 0;
|
||||
if (dataRows.length > RAW_RESULTS_LIMIT) {
|
||||
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
|
||||
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
|
||||
}
|
||||
|
||||
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex + this.props.offset}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
selectedColumn={
|
||||
this.state.selectedItem?.row === rowIndex
|
||||
? this.state.selectedItem?.column
|
||||
: undefined
|
||||
}
|
||||
onSelected={this.setSelection}
|
||||
scroller={this.scroller}
|
||||
/>
|
||||
));
|
||||
|
||||
if (numTruncatedResults > 0) {
|
||||
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
|
||||
tableRows.push(
|
||||
<tr>
|
||||
<td
|
||||
key={"message"}
|
||||
colSpan={colSpan}
|
||||
style={{ textAlign: "center", fontStyle: "italic" }}
|
||||
>
|
||||
Too many results to show at once. {numTruncatedResults} result(s)
|
||||
omitted.
|
||||
</td>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={this.props.sortState}
|
||||
/>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
if (numTruncatedResults > 0) {
|
||||
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
|
||||
tableRows.push(
|
||||
<tr>
|
||||
<td
|
||||
key={"message"}
|
||||
colSpan={colSpan}
|
||||
style={{ textAlign: "center", fontStyle: "italic" }}
|
||||
>
|
||||
Too many results to show at once. {numTruncatedResults} result(s)
|
||||
omitted.
|
||||
</td>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
private handleNavigationEvent(event: NavigateMsg) {
|
||||
switch (event.direction) {
|
||||
case NavigationDirection.up: {
|
||||
this.navigateWithDelta(-1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.down: {
|
||||
this.navigateWithDelta(1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.left: {
|
||||
this.navigateWithDelta(0, -1);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.right: {
|
||||
this.navigateWithDelta(0, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private navigateWithDelta(rowDelta: number, columnDelta: number) {
|
||||
this.setState((prevState) => {
|
||||
const numberOfAlerts = this.props.resultSet.rows.length;
|
||||
if (numberOfAlerts === 0) {
|
||||
return prevState;
|
||||
}
|
||||
const currentRow = prevState.selectedItem?.row;
|
||||
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
|
||||
if (nextRow < 0 || nextRow >= numberOfAlerts) {
|
||||
return prevState;
|
||||
}
|
||||
const currentColumn = prevState.selectedItem?.column;
|
||||
const nextColumn =
|
||||
currentColumn === undefined ? 0 : currentColumn + columnDelta;
|
||||
// Jump to the location of the new cell
|
||||
const rowData = this.props.resultSet.rows[nextRow];
|
||||
if (nextColumn < 0 || nextColumn >= rowData.length) {
|
||||
return prevState;
|
||||
}
|
||||
const cellData = rowData[nextColumn];
|
||||
if (cellData != null && typeof cellData === "object") {
|
||||
const location = tryGetResolvableLocation(cellData.url);
|
||||
if (location !== undefined) {
|
||||
jumpToLocation(location, this.props.databaseUri);
|
||||
}
|
||||
}
|
||||
this.scroller.scrollIntoViewOnNextUpdate();
|
||||
return {
|
||||
...prevState,
|
||||
selectedItem: { row: nextRow, column: nextColumn },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.scroller.update();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.scroller.update();
|
||||
onNavigation.addListener(this.handleNavigationEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
onNavigation.removeListener(this.handleNavigationEvent);
|
||||
}
|
||||
return (
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={sortState}
|
||||
/>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user