mirror of https://github.com/apache/druid.git
Web console: add table and column search (#15990)
* Make a search * fix snapshot * added message when not found
This commit is contained in:
parent
101176590c
commit
c5b032799c
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue