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 sys
</option> </option>
</HTMLSelect> </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 <div
className="tree-container" className="tree-container"
> >

View File

@ -34,18 +34,19 @@
} }
.schema-selector { .schema-selector {
position: absolute;
top: 0;
select { select {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
} }
.search-box {
margin: 2px 8px;
}
.tree-container { .tree-container {
position: absolute; position: absolute;
top: 40px; top: 78px;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
overflow: auto; overflow: auto;
@ -57,11 +58,20 @@
.highlight { .highlight {
animation: druid-glow 1s infinite alternate; animation: druid-glow 1s infinite alternate;
} }
}
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover2-target {
width: 188px; width: 188px;
display: inline-block; display: inline-block;
cursor: pointer; 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 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 { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2'; import { Popover2 } from '@blueprintjs/popover2';
import type { SqlExpression } from '@druid-toolkit/query'; import type { SqlExpression } from '@druid-toolkit/query';
@ -37,7 +49,14 @@ import React from 'react';
import { Deferred, Loader } from '../../../components'; import { Deferred, Loader } from '../../../components';
import type { ColumnMetadata } from '../../../utils'; import type { ColumnMetadata } from '../../../utils';
import { copyAndAlert, dataTypeToIcon, groupBy, oneOf, prettyPrintSql } from '../../../utils'; import {
copyAndAlert,
dataTypeToIcon,
groupBy,
oneOf,
prettyPrintSql,
tickIcon,
} from '../../../utils';
import { import {
ComplexMenuItems, ComplexMenuItems,
@ -81,6 +100,16 @@ interface HandleColumnClickOptions {
onQueryChange: (query: SqlQuery, run: boolean) => void; 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 { function handleColumnShow(options: HandleColumnClickOptions): void {
const { const {
columnSchema, columnSchema,
@ -147,6 +176,14 @@ export interface ColumnTreeState {
columnTree?: TreeNodeInfo[]; columnTree?: TreeNodeInfo[];
currentSchemaSubtree?: TreeNodeInfo[]; currentSchemaSubtree?: TreeNodeInfo[];
selectedTreeIndex: number; 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) { export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
@ -181,8 +218,15 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
onQueryChange, onQueryChange,
highlightTable, highlightTable,
} = props; } = 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( const columnTree = groupBy(
columnMetadata, columnMetadata,
r => r.TABLE_SCHEMA, r => r.TABLE_SCHEMA,
@ -190,12 +234,27 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
id: schemaName, id: schemaName,
label: schemaName, label: schemaName,
childNodes: groupBy( 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, r => r.TABLE_NAME,
(metadata, tableName): TreeNodeInfo => ({ (metadata, tableName): TreeNodeInfo => ({
id: tableName, id: tableName,
icon: IconNames.TH, icon: IconNames.TH,
className: tableName === highlightTable ? 'highlight' : undefined, className: tableName === highlightTable ? 'highlight' : undefined,
isExpanded:
isSearching &&
(searchMode === 'columns-only' ||
!tableName.toLowerCase().includes(lowerSearchString)),
label: ( label: (
<Popover2 <Popover2
position={Position.RIGHT} position={Position.RIGHT}
@ -513,12 +572,10 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
if (selectedTreeIndex > -1) { if (selectedTreeIndex > -1) {
const treeNodes = columnTree[selectedTreeIndex].childNodes; const treeNodes = columnTree[selectedTreeIndex].childNodes;
if (treeNodes) { if (treeNodes && defaultTable) {
if (defaultTable) { expandedNode = treeNodes.findIndex(node => {
expandedNode = treeNodes.findIndex(node => { return node.id === defaultTable;
return node.id === defaultTable; });
});
}
} }
} }
@ -536,6 +593,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnTree, columnTree,
selectedTreeIndex, selectedTreeIndex,
currentSchemaSubtree, currentSchemaSubtree,
prevSearchHash: searchHash,
}; };
} }
return null; return null;
@ -545,12 +603,13 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
super(props); super(props);
this.state = { this.state = {
selectedTreeIndex: -1, selectedTreeIndex: -1,
searchString: '',
searchMode: 'tables-and-columns',
}; };
} }
private renderSchemaSelector() { private renderSchemaSelector() {
const { columnTree, selectedTreeIndex } = this.state; const { columnTree, selectedTreeIndex } = this.state;
if (!columnTree) return null;
return ( return (
<HTMLSelect <HTMLSelect
@ -561,7 +620,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
minimal minimal
large large
> >
{columnTree.map((treeNode, i) => ( {columnTree?.map((treeNode, i) => (
<option key={i} value={i}> <option key={i} value={i}>
{treeNode.label} {treeNode.label}
</option> </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 => { private readonly handleSchemaSelectorChange = (e: ChangeEvent<HTMLSelectElement>): void => {
const { columnTree } = this.state; const { columnTree } = this.state;
if (!columnTree) return; if (!columnTree) return;
@ -597,7 +696,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
render() { render() {
const { columnMetadataLoading } = this.props; const { columnMetadataLoading } = this.props;
const { currentSchemaSubtree } = this.state; const { currentSchemaSubtree, searchString } = this.state;
if (columnMetadataLoading) { if (columnMetadataLoading) {
return ( return (
@ -612,12 +711,19 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
return ( return (
<div className="column-tree"> <div className="column-tree">
{this.renderSchemaSelector()} {this.renderSchemaSelector()}
{this.renderSearch()}
<div className="tree-container"> <div className="tree-container">
<Tree {currentSchemaSubtree.length ? (
contents={currentSchemaSubtree} <Tree
onNodeCollapse={this.handleNodeCollapse} contents={currentSchemaSubtree}
onNodeExpand={this.handleNodeExpand} onNodeCollapse={this.handleNodeCollapse}
/> onNodeExpand={this.handleNodeExpand}
/>
) : (
<div className="message-box">
{searchString ? 'The search returned no results' : 'No tables'}
</div>
)}
</div> </div>
</div> </div>
); );