Web console: add table and column search (#15990)

* Make a search

* fix snapshot

* added message when not found
This commit is contained in:
Vadim Ogievetsky 2024-02-29 15:45:50 -08:00 committed by GitHub
parent 101176590c
commit c5b032799c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 220 additions and 27 deletions

View File

@ -23,6 +23,83 @@ exports[`ColumnTree matches snapshot 1`] = `
sys
</option>
</HTMLSelect>
<Blueprint4.InputGroup
className="search-box"
onChange={[Function]}
placeholder="Search"
rightElement={
<Blueprint4.ButtonGroup
minimal={true}
>
<Blueprint4.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint4.Menu>
<Blueprint4.MenuDivider
title="Search in"
/>
<Blueprint4.MenuItem
active={false}
disabled={false}
icon="tick"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
selected={false}
shouldDismissPopover={true}
text="Tables and columns"
/>
<Blueprint4.MenuItem
active={false}
disabled={false}
icon="blank"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
selected={false}
shouldDismissPopover={true}
text="Tables only"
/>
<Blueprint4.MenuItem
active={false}
disabled={false}
icon="blank"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
selected={false}
shouldDismissPopover={true}
text="Columns only"
/>
</Blueprint4.Menu>
}
defaultIsOpen={false}
disabled={false}
fill={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
matchTargetWidth={false}
minimal={false}
openOnTargetFocus={true}
position="bottom-left"
positioningStrategy="absolute"
shouldReturnFocusOnClose={false}
targetTagName="span"
transitionDuration={300}
usePortal={true}
>
<Blueprint4.Button
icon="settings"
/>
</Blueprint4.Popover2>
</Blueprint4.ButtonGroup>
}
value=""
/>
<div
className="tree-container"
>

View File

@ -34,18 +34,19 @@
}
.schema-selector {
position: absolute;
top: 0;
select {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
.search-box {
margin: 2px 8px;
}
.tree-container {
position: absolute;
top: 40px;
top: 78px;
bottom: 0;
width: 100%;
overflow: auto;
@ -57,11 +58,20 @@
.highlight {
animation: druid-glow 1s infinite alternate;
}
}
.#{$bp-ns}-popover2-target {
width: 188px;
display: inline-block;
cursor: pointer;
.#{$bp-ns}-popover2-target {
width: 188px;
display: inline-block;
cursor: pointer;
}
.message-box {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
text-align: center;
}
}
}

View File

@ -17,7 +17,19 @@
*/
import type { TreeNodeInfo } from '@blueprintjs/core';
import { Classes, HTMLSelect, Icon, Menu, MenuItem, Position, Tree } from '@blueprintjs/core';
import {
Button,
ButtonGroup,
Classes,
HTMLSelect,
Icon,
InputGroup,
Menu,
MenuDivider,
MenuItem,
Position,
Tree,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import type { SqlExpression } from '@druid-toolkit/query';
@ -37,7 +49,14 @@ import React from 'react';
import { Deferred, Loader } from '../../../components';
import type { ColumnMetadata } from '../../../utils';
import { copyAndAlert, dataTypeToIcon, groupBy, oneOf, prettyPrintSql } from '../../../utils';
import {
copyAndAlert,
dataTypeToIcon,
groupBy,
oneOf,
prettyPrintSql,
tickIcon,
} from '../../../utils';
import {
ComplexMenuItems,
@ -81,6 +100,16 @@ interface HandleColumnClickOptions {
onQueryChange: (query: SqlQuery, run: boolean) => void;
}
type SearchMode = 'tables-and-columns' | 'tables-only' | 'columns-only';
const SEARCH_MODES: SearchMode[] = ['tables-and-columns', 'tables-only', 'columns-only'];
const SEARCH_MDOE_TITLE: Record<SearchMode, string> = {
'tables-and-columns': 'Tables and columns',
'tables-only': 'Tables only',
'columns-only': 'Columns only',
};
function handleColumnShow(options: HandleColumnClickOptions): void {
const {
columnSchema,
@ -147,6 +176,14 @@ export interface ColumnTreeState {
columnTree?: TreeNodeInfo[];
currentSchemaSubtree?: TreeNodeInfo[];
selectedTreeIndex: number;
searchString: string;
searchMode: SearchMode;
prevSearchHash?: string;
}
function computeSearchHash(searchString: string, searchMode: SearchMode): string {
if (!searchString) return '';
return `${searchString.toLowerCase()}_${searchMode}`;
}
export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
@ -181,8 +218,15 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
onQueryChange,
highlightTable,
} = props;
const { searchString, searchMode } = state;
const searchHash = computeSearchHash(searchString, searchMode);
if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
if (
columnMetadata &&
(columnMetadata !== state.prevColumnMetadata || searchHash !== state.prevSearchHash)
) {
const lowerSearchString = searchString.toLowerCase();
const isSearching = Boolean(lowerSearchString);
const columnTree = groupBy(
columnMetadata,
r => r.TABLE_SCHEMA,
@ -190,12 +234,27 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
id: schemaName,
label: schemaName,
childNodes: groupBy(
metadata,
isSearching
? metadata.filter(
r =>
(searchMode === 'tables-and-columns' &&
(r.TABLE_NAME.toLowerCase().includes(lowerSearchString) ||
r.COLUMN_NAME.toLowerCase().includes(lowerSearchString))) ||
(searchMode === 'tables-only' &&
r.TABLE_NAME.toLowerCase().includes(lowerSearchString)) ||
(searchMode === 'columns-only' &&
r.COLUMN_NAME.toLowerCase().includes(lowerSearchString)),
)
: metadata,
r => r.TABLE_NAME,
(metadata, tableName): TreeNodeInfo => ({
id: tableName,
icon: IconNames.TH,
className: tableName === highlightTable ? 'highlight' : undefined,
isExpanded:
isSearching &&
(searchMode === 'columns-only' ||
!tableName.toLowerCase().includes(lowerSearchString)),
label: (
<Popover2
position={Position.RIGHT}
@ -513,12 +572,10 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
if (selectedTreeIndex > -1) {
const treeNodes = columnTree[selectedTreeIndex].childNodes;
if (treeNodes) {
if (defaultTable) {
expandedNode = treeNodes.findIndex(node => {
return node.id === defaultTable;
});
}
if (treeNodes && defaultTable) {
expandedNode = treeNodes.findIndex(node => {
return node.id === defaultTable;
});
}
}
@ -536,6 +593,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnTree,
selectedTreeIndex,
currentSchemaSubtree,
prevSearchHash: searchHash,
};
}
return null;
@ -545,12 +603,13 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
super(props);
this.state = {
selectedTreeIndex: -1,
searchString: '',
searchMode: 'tables-and-columns',
};
}
private renderSchemaSelector() {
const { columnTree, selectedTreeIndex } = this.state;
if (!columnTree) return null;
return (
<HTMLSelect
@ -561,7 +620,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
minimal
large
>
{columnTree.map((treeNode, i) => (
{columnTree?.map((treeNode, i) => (
<option key={i} value={i}>
{treeNode.label}
</option>
@ -570,6 +629,46 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
);
}
private renderSearch() {
const { searchString, searchMode } = this.state;
return (
<InputGroup
className="search-box"
placeholder="Search"
value={searchString}
onChange={e => {
this.setState({ searchString: e.target.value.substring(0, 100) });
}}
rightElement={
<ButtonGroup minimal>
{searchString !== '' && (
<Button icon={IconNames.CROSS} onClick={() => this.setState({ searchString: '' })} />
)}
<Popover2
position="bottom-left"
content={
<Menu>
<MenuDivider title="Search in" />
{SEARCH_MODES.map(mode => (
<MenuItem
key={mode}
icon={tickIcon(mode === searchMode)}
text={SEARCH_MDOE_TITLE[mode]}
onClick={() => this.setState({ searchMode: mode })}
/>
))}
</Menu>
}
>
<Button icon={IconNames.SETTINGS} />
</Popover2>
</ButtonGroup>
}
/>
);
}
private readonly handleSchemaSelectorChange = (e: ChangeEvent<HTMLSelectElement>): void => {
const { columnTree } = this.state;
if (!columnTree) return;
@ -597,7 +696,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
render() {
const { columnMetadataLoading } = this.props;
const { currentSchemaSubtree } = this.state;
const { currentSchemaSubtree, searchString } = this.state;
if (columnMetadataLoading) {
return (
@ -612,12 +711,19 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
return (
<div className="column-tree">
{this.renderSchemaSelector()}
{this.renderSearch()}
<div className="tree-container">
<Tree
contents={currentSchemaSubtree}
onNodeCollapse={this.handleNodeCollapse}
onNodeExpand={this.handleNodeExpand}
/>
{currentSchemaSubtree.length ? (
<Tree
contents={currentSchemaSubtree}
onNodeCollapse={this.handleNodeCollapse}
onNodeExpand={this.handleNodeExpand}
/>
) : (
<div className="message-box">
{searchString ? 'The search returned no results' : 'No tables'}
</div>
)}
</div>
</div>
);