mirror of https://github.com/apache/druid.git
Web console: fix grouped filtering and add complex menu (#14668)
* fix filtering when grouped * add complex menu * complex aggs * use ResizeObserverEntry * add quantile and test * fix style * update snapshots
This commit is contained in:
parent
2e456d25ae
commit
153948198c
|
@ -40,7 +40,12 @@ export function bootstrapReactTable() {
|
||||||
className: DEFAULT_TABLE_CLASS_NAME,
|
className: DEFAULT_TABLE_CLASS_NAME,
|
||||||
defaultFilterMethod: (filter: Filter, row: any) => {
|
defaultFilterMethod: (filter: Filter, row: any) => {
|
||||||
const id = filter.pivotId || filter.id;
|
const id = filter.pivotId || filter.id;
|
||||||
return booleanCustomTableFilter(filter, row[id]);
|
const subRows = row._subRows;
|
||||||
|
if (Array.isArray(subRows)) {
|
||||||
|
return subRows.some(r => booleanCustomTableFilter(filter, r[id]));
|
||||||
|
} else {
|
||||||
|
return booleanCustomTableFilter(filter, row[id]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
LoadingComponent: Loader,
|
LoadingComponent: Loader,
|
||||||
loadingText: '',
|
loadingText: '',
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IResizeEntry } from '@blueprintjs/core';
|
|
||||||
import { FormGroup, HTMLSelect, Radio, RadioGroup, ResizeSensor } from '@blueprintjs/core';
|
import { FormGroup, HTMLSelect, Radio, RadioGroup, ResizeSensor } from '@blueprintjs/core';
|
||||||
import type { AxisScale } from 'd3-axis';
|
import type { AxisScale } from 'd3-axis';
|
||||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||||
|
@ -428,7 +427,7 @@ ORDER BY "start" DESC`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly handleResize = (entries: IResizeEntry[]) => {
|
private readonly handleResize = (entries: ResizeObserverEntry[]) => {
|
||||||
const chartRect = entries[0].contentRect;
|
const chartRect = entries[0].contentRect;
|
||||||
this.setState({
|
this.setState({
|
||||||
chartWidth: chartRect.width,
|
chartWidth: chartRect.width,
|
||||||
|
|
|
@ -107,7 +107,7 @@ export function addOrUpdateFilter(filters: readonly Filter[], filter: Filter): F
|
||||||
return addOrUpdate(filters, filter, f => f.id);
|
return addOrUpdate(filters, filter, f => f.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function booleanCustomTableFilter(filter: Filter, value: any): boolean {
|
export function booleanCustomTableFilter(filter: Filter, value: unknown): boolean {
|
||||||
if (value == null) return false;
|
if (value == null) return false;
|
||||||
const modeAndNeedle = parseFilterModeAndNeedle(filter);
|
const modeAndNeedle = parseFilterModeAndNeedle(filter);
|
||||||
if (!modeAndNeedle) return true;
|
if (!modeAndNeedle) return true;
|
||||||
|
|
|
@ -32,7 +32,13 @@ exports[`ColumnTree matches snapshot 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"childNodes": Array [
|
"childNodes": Array [
|
||||||
Object {
|
Object {
|
||||||
"icon": "time",
|
"icon": <Blueprint4.Icon
|
||||||
|
aria-hidden={true}
|
||||||
|
className="bp4-tree-node-icon"
|
||||||
|
icon="time"
|
||||||
|
tabIndex={-1}
|
||||||
|
title="TIMESTAMP"
|
||||||
|
/>,
|
||||||
"id": "__time",
|
"id": "__time",
|
||||||
"label": <Blueprint4.Popover2
|
"label": <Blueprint4.Popover2
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
@ -65,7 +71,13 @@ exports[`ColumnTree matches snapshot 1`] = `
|
||||||
</Blueprint4.Popover2>,
|
</Blueprint4.Popover2>,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"icon": "numerical",
|
"icon": <Blueprint4.Icon
|
||||||
|
aria-hidden={true}
|
||||||
|
className="bp4-tree-node-icon"
|
||||||
|
icon="numerical"
|
||||||
|
tabIndex={-1}
|
||||||
|
title="BIGINT"
|
||||||
|
/>,
|
||||||
"id": "added",
|
"id": "added",
|
||||||
"label": <Blueprint4.Popover2
|
"label": <Blueprint4.Popover2
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
@ -98,7 +110,13 @@ exports[`ColumnTree matches snapshot 1`] = `
|
||||||
</Blueprint4.Popover2>,
|
</Blueprint4.Popover2>,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"icon": "floating-point",
|
"icon": <Blueprint4.Icon
|
||||||
|
aria-hidden={true}
|
||||||
|
className="bp4-tree-node-icon"
|
||||||
|
icon="floating-point"
|
||||||
|
tabIndex={-1}
|
||||||
|
title="FLOAT"
|
||||||
|
/>,
|
||||||
"id": "addedBy10",
|
"id": "addedBy10",
|
||||||
"label": <Blueprint4.Popover2
|
"label": <Blueprint4.Popover2
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ComplexMenuItems matches snapshot when menu is opened for column not inside group by 1`] = `
|
||||||
|
<div>
|
||||||
|
<li
|
||||||
|
class="bp4-submenu"
|
||||||
|
role="none"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bp4-popover-wrapper"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-haspopup="true"
|
||||||
|
class="bp4-popover-target"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="bp4-menu-item"
|
||||||
|
role="none"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bp4-menu-item-icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="bp4-icon bp4-icon-function"
|
||||||
|
icon="function"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
data-icon="function"
|
||||||
|
height="16"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.12 4.74H6.98c.33-1.29.75-2.24 1.28-2.84.33-.37.64-.56.95-.56.06 0 .11.02.15.05.04.04.06.09.06.15 0 .05-.04.15-.13.29-.09.14-.13.28-.13.4 0 .18.07.33.2.46.14.13.31.19.52.19.22 0 .41-.08.56-.23.15-.16.23-.37.23-.63 0-.3-.11-.55-.34-.74C10.1 1.09 9.74 1 9.24 1c-.78 0-1.49.22-2.12.67-.64.45-1.24 1.2-1.81 2.23-.2.36-.38.59-.56.69-.18.1-.46.15-.85.15l-.26.9h1.08l-1.59 6.12c-.27 1.01-.44 1.63-.54 1.86-.14.34-.34.63-.62.87-.11.1-.24.15-.4.15a.15.15 0 01-.11-.04l-.04-.05c0-.03.04-.08.12-.16.08-.08.12-.2.12-.36 0-.18-.06-.33-.19-.44-.12-.12-.3-.18-.54-.18-.28 0-.51.08-.68.23-.16.14-.25.32-.25.53 0 .22.1.42.31.59.21.17.53.25.97.25.7 0 1.32-.18 1.87-.54.54-.36 1.02-.92 1.42-1.67.41-.75.82-1.96 1.25-3.63l.91-3.54h1.1l.29-.89zm5.43 1.52c.2-.15.41-.23.62-.23.08 0 .23.03.45.09s.41.09.57.09c.23 0 .42-.08.57-.23.16-.16.24-.36.24-.61 0-.26-.08-.47-.23-.62-.15-.15-.37-.23-.66-.23-.25 0-.5.06-.72.18-.23.12-.51.38-.86.78-.26.3-.64.81-1.15 1.55-.2-.91-.55-1.75-1.05-2.51l-2.72.46-.06.29c.2-.04.37-.06.51-.06.27 0 .49.11.67.34.28.36.67 1.45 1.17 3.26-.39.52-.66.85-.8 1.01-.24.26-.44.42-.59.5-.12.06-.25.09-.41.09-.11 0-.3-.06-.56-.18-.18-.08-.34-.12-.48-.12-.27 0-.48.08-.66.25-.17.17-.26.38-.26.64 0 .25.08.44.24.6.16.15.37.23.64.23.26 0 .5-.05.73-.16.23-.11.52-.34.86-.69.35-.35.82-.9 1.43-1.67.23.73.44 1.25.61 1.58s.37.57.59.71c.22.15.5.22.83.22.32 0 .65-.11.98-.34.44-.3.88-.81 1.34-1.53l-.26-.15c-.31.43-.54.7-.69.8-.1.07-.22.1-.35.1-.16 0-.32-.1-.48-.3-.27-.34-.62-1.27-1.06-2.8.4-.68.73-1.13 1-1.34z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="bp4-fill bp4-text-overflow-ellipsis"
|
||||||
|
>
|
||||||
|
Aggregate
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="bp4-icon bp4-icon-caret-right bp4-submenu-icon"
|
||||||
|
icon="caret-right"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
data-icon="caret-right"
|
||||||
|
height="16"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SqlQuery } from '@druid-toolkit/query';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ComplexMenuItems } from './complex-menu-items';
|
||||||
|
|
||||||
|
describe('ComplexMenuItems', () => {
|
||||||
|
it('matches snapshot when menu is opened for column not inside group by', () => {
|
||||||
|
const numberMenu = (
|
||||||
|
<ComplexMenuItems
|
||||||
|
schema="schema"
|
||||||
|
table="table"
|
||||||
|
columnName="user_theta"
|
||||||
|
columnType="COMPLEX<thetaSketch>"
|
||||||
|
parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
|
||||||
|
onQueryChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(numberMenu);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MenuItem } from '@blueprintjs/core';
|
||||||
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
|
import type { SqlExpression, SqlQuery } from '@druid-toolkit/query';
|
||||||
|
import { C, F } from '@druid-toolkit/query';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { prettyPrintSql } from '../../../../../utils';
|
||||||
|
|
||||||
|
const UNIQUE_FUNCTIONS: Record<string, string> = {
|
||||||
|
'COMPLEX<hyperUnique>': 'APPROX_COUNT_DISTINCT_BUILTIN',
|
||||||
|
'COMPLEX<thetaSketch>': 'APPROX_COUNT_DISTINCT_DS_THETA',
|
||||||
|
'COMPLEX<HLLSketch>': 'APPROX_COUNT_DISTINCT_DS_HLL',
|
||||||
|
};
|
||||||
|
|
||||||
|
const QUANTILE_FUNCTIONS: Record<string, string> = {
|
||||||
|
'COMPLEX<quantilesDoublesSketch>': 'APPROX_QUANTILE_DS',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ComplexMenuItemsProps {
|
||||||
|
table: string;
|
||||||
|
schema: string;
|
||||||
|
columnName: string;
|
||||||
|
columnType: string;
|
||||||
|
parsedQuery: SqlQuery;
|
||||||
|
onQueryChange: (query: SqlQuery, run?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComplexMenuItems = React.memo(function ComplexMenuItems(props: ComplexMenuItemsProps) {
|
||||||
|
const { columnName, columnType, parsedQuery, onQueryChange } = props;
|
||||||
|
const column = C(columnName);
|
||||||
|
|
||||||
|
function renderAggregateMenu(): JSX.Element | undefined {
|
||||||
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
|
function aggregateMenuItem(ex: SqlExpression, alias: string) {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
text={prettyPrintSql(ex)}
|
||||||
|
onClick={() => {
|
||||||
|
onQueryChange(parsedQuery.addSelect(ex.as(alias)), true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueFn = UNIQUE_FUNCTIONS[columnType];
|
||||||
|
const quantileFn = QUANTILE_FUNCTIONS[columnType];
|
||||||
|
if (!uniqueFn && !quantileFn) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||||
|
{uniqueFn && aggregateMenuItem(F(uniqueFn, column), `unique_${columnName}`)}
|
||||||
|
{quantileFn && (
|
||||||
|
<>
|
||||||
|
{aggregateMenuItem(F(quantileFn, column, 0.5), `median_${columnName}`)}
|
||||||
|
{aggregateMenuItem(F(quantileFn, column, 0.98), `p98_${columnName}`)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{renderAggregateMenu()}</>;
|
||||||
|
});
|
|
@ -16,6 +16,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from './complex-menu-items/complex-menu-items';
|
||||||
export * from './number-menu-items/number-menu-items';
|
export * from './number-menu-items/number-menu-items';
|
||||||
export * from './string-menu-items/string-menu-items';
|
export * from './string-menu-items/string-menu-items';
|
||||||
export * from './time-menu-items/time-menu-items';
|
export * from './time-menu-items/time-menu-items';
|
||||||
|
|
|
@ -36,9 +36,10 @@ export interface NumberMenuItemsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberMenuItems = React.memo(function NumberMenuItems(props: NumberMenuItemsProps) {
|
export const NumberMenuItems = React.memo(function NumberMenuItems(props: NumberMenuItemsProps) {
|
||||||
function renderFilterMenu(): JSX.Element {
|
const { columnName, parsedQuery, onQueryChange } = props;
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
const column = C(columnName);
|
||||||
|
|
||||||
|
function renderFilterMenu(): JSX.Element {
|
||||||
function filterMenuItem(clause: SqlExpression) {
|
function filterMenuItem(clause: SqlExpression) {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -50,7 +51,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FILTER} text="Filter">
|
<MenuItem icon={IconNames.FILTER} text="Filter">
|
||||||
{filterMenuItem(column.greaterThan(NINE_THOUSAND))}
|
{filterMenuItem(column.greaterThan(NINE_THOUSAND))}
|
||||||
|
@ -60,7 +60,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveFilter(): JSX.Element | undefined {
|
function renderRemoveFilter(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -75,7 +74,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGroupByMenu(): JSX.Element | undefined {
|
function renderGroupByMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function groupByMenuItem(ex: SqlExpression, alias?: string) {
|
function groupByMenuItem(ex: SqlExpression, alias?: string) {
|
||||||
|
@ -95,7 +93,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
||||||
{groupByMenuItem(column)}
|
{groupByMenuItem(column)}
|
||||||
|
@ -105,7 +102,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveGroupBy(): JSX.Element | undefined {
|
function renderRemoveGroupBy(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
||||||
if (!groupedSelectIndexes.length) return;
|
if (!groupedSelectIndexes.length) return;
|
||||||
|
|
||||||
|
@ -121,7 +117,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAggregateMenu(): JSX.Element | undefined {
|
function renderAggregateMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function aggregateMenuItem(ex: SqlExpression, alias: string) {
|
function aggregateMenuItem(ex: SqlExpression, alias: string) {
|
||||||
|
@ -135,7 +130,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||||
{aggregateMenuItem(F('SUM', column), `sum_${columnName}`)}
|
{aggregateMenuItem(F('SUM', column), `sum_${columnName}`)}
|
||||||
|
|
|
@ -35,9 +35,10 @@ export interface StringMenuItemsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StringMenuItems = React.memo(function StringMenuItems(props: StringMenuItemsProps) {
|
export const StringMenuItems = React.memo(function StringMenuItems(props: StringMenuItemsProps) {
|
||||||
function renderFilterMenu(): JSX.Element | undefined {
|
const { schema, table, columnName, parsedQuery, onQueryChange } = props;
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
const column = C(columnName);
|
||||||
|
|
||||||
|
function renderFilterMenu(): JSX.Element | undefined {
|
||||||
function filterMenuItem(clause: SqlExpression, run = true) {
|
function filterMenuItem(clause: SqlExpression, run = true) {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -49,7 +50,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FILTER} text="Filter">
|
<MenuItem icon={IconNames.FILTER} text="Filter">
|
||||||
{filterMenuItem(column.isNotNull())}
|
{filterMenuItem(column.isNotNull())}
|
||||||
|
@ -61,7 +61,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveFilter(): JSX.Element | undefined {
|
function renderRemoveFilter(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -76,7 +75,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveGroupBy(): JSX.Element | undefined {
|
function renderRemoveGroupBy(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
||||||
if (!groupedSelectIndexes.length) return;
|
if (!groupedSelectIndexes.length) return;
|
||||||
|
|
||||||
|
@ -92,7 +90,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGroupByMenu(): JSX.Element | undefined {
|
function renderGroupByMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function groupByMenuItem(ex: SqlExpression, alias?: string) {
|
function groupByMenuItem(ex: SqlExpression, alias?: string) {
|
||||||
|
@ -112,7 +109,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
||||||
{groupByMenuItem(column)}
|
{groupByMenuItem(column)}
|
||||||
|
@ -123,7 +119,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAggregateMenu(): JSX.Element | undefined {
|
function renderAggregateMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function aggregateMenuItem(ex: SqlExpression, alias: string, run = true) {
|
function aggregateMenuItem(ex: SqlExpression, alias: string, run = true) {
|
||||||
|
@ -137,7 +132,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||||
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
|
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
|
||||||
|
@ -152,7 +146,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderJoinMenu(): JSX.Element | undefined {
|
function renderJoinMenu(): JSX.Element | undefined {
|
||||||
const { schema, table, columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (schema !== 'lookup' || !parsedQuery) return;
|
if (schema !== 'lookup' || !parsedQuery) return;
|
||||||
const firstTableName = parsedQuery.getFirstTableName();
|
const firstTableName = parsedQuery.getFirstTableName();
|
||||||
if (!firstTableName) return;
|
if (!firstTableName) return;
|
||||||
|
@ -212,7 +205,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveJoin(): JSX.Element | undefined {
|
function renderRemoveJoin(): JSX.Element | undefined {
|
||||||
const { schema, parsedQuery, onQueryChange } = props;
|
|
||||||
if (schema !== 'lookup' || !parsedQuery) return;
|
if (schema !== 'lookup' || !parsedQuery) return;
|
||||||
if (!parsedQuery.hasJoin()) return;
|
if (!parsedQuery.hasJoin()) return;
|
||||||
|
|
||||||
|
|
|
@ -113,9 +113,10 @@ export interface TimeMenuItemsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuItemsProps) {
|
export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuItemsProps) {
|
||||||
function renderFilterMenu(): JSX.Element | undefined {
|
const { columnName, parsedQuery, onQueryChange } = props;
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
const column = C(columnName);
|
||||||
|
|
||||||
|
function renderFilterMenu(): JSX.Element | undefined {
|
||||||
function filterMenuItem(label: string, clause: SqlExpression) {
|
function filterMenuItem(label: string, clause: SqlExpression) {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -161,7 +162,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveFilter(): JSX.Element | undefined {
|
function renderRemoveFilter(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -176,7 +176,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemoveGroupBy(): JSX.Element | undefined {
|
function renderRemoveGroupBy(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName);
|
||||||
if (!groupedSelectIndexes.length) return;
|
if (!groupedSelectIndexes.length) return;
|
||||||
|
|
||||||
|
@ -192,7 +191,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGroupByMenu(): JSX.Element | undefined {
|
function renderGroupByMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function groupByMenuItem(ex: SqlExpression, alias: string) {
|
function groupByMenuItem(ex: SqlExpression, alias: string) {
|
||||||
|
@ -212,7 +210,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
<MenuItem icon={IconNames.GROUP_OBJECTS} text="Group by">
|
||||||
{groupByMenuItem(F.timeFloor(column, 'PT1H'), `${columnName}_by_hour`)}
|
{groupByMenuItem(F.timeFloor(column, 'PT1H'), `${columnName}_by_hour`)}
|
||||||
|
@ -229,7 +226,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAggregateMenu(): JSX.Element | undefined {
|
function renderAggregateMenu(): JSX.Element | undefined {
|
||||||
const { columnName, parsedQuery, onQueryChange } = props;
|
|
||||||
if (!parsedQuery.hasGroupBy()) return;
|
if (!parsedQuery.hasGroupBy()) return;
|
||||||
|
|
||||||
function aggregateMenuItem(ex: SqlExpression, alias: string) {
|
function aggregateMenuItem(ex: SqlExpression, alias: string) {
|
||||||
|
@ -243,7 +239,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = C(columnName);
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||||
{aggregateMenuItem(F.max(column), `max_${columnName}`)}
|
{aggregateMenuItem(F.max(column), `max_${columnName}`)}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TreeNodeInfo } from '@blueprintjs/core';
|
import type { TreeNodeInfo } from '@blueprintjs/core';
|
||||||
import { HTMLSelect, Menu, MenuItem, Position, Tree } from '@blueprintjs/core';
|
import { Classes, HTMLSelect, Icon, Menu, 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';
|
||||||
|
@ -39,7 +39,12 @@ 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 } from '../../../utils';
|
||||||
|
|
||||||
import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-menu';
|
import {
|
||||||
|
ComplexMenuItems,
|
||||||
|
NumberMenuItems,
|
||||||
|
StringMenuItems,
|
||||||
|
TimeMenuItems,
|
||||||
|
} from './column-tree-menu';
|
||||||
|
|
||||||
import './column-tree.scss';
|
import './column-tree.scss';
|
||||||
|
|
||||||
|
@ -400,7 +405,15 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
||||||
childNodes: metadata.map(
|
childNodes: metadata.map(
|
||||||
(columnData): TreeNodeInfo => ({
|
(columnData): TreeNodeInfo => ({
|
||||||
id: columnData.COLUMN_NAME,
|
id: columnData.COLUMN_NAME,
|
||||||
icon: dataTypeToIcon(columnData.DATA_TYPE),
|
icon: (
|
||||||
|
<Icon
|
||||||
|
className={Classes.TREE_NODE_ICON}
|
||||||
|
icon={dataTypeToIcon(columnData.DATA_TYPE)}
|
||||||
|
aria-hidden
|
||||||
|
tabIndex={-1}
|
||||||
|
title={columnData.DATA_TYPE}
|
||||||
|
/>
|
||||||
|
),
|
||||||
label: (
|
label: (
|
||||||
<Popover2
|
<Popover2
|
||||||
position={Position.RIGHT}
|
position={Position.RIGHT}
|
||||||
|
@ -454,6 +467,16 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
||||||
onQueryChange={onQueryChange}
|
onQueryChange={onQueryChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{parsedQuery && columnData.DATA_TYPE.startsWith('COMPLEX<') && (
|
||||||
|
<ComplexMenuItems
|
||||||
|
table={tableName}
|
||||||
|
schema={schemaName}
|
||||||
|
columnName={columnData.COLUMN_NAME}
|
||||||
|
columnType={columnData.DATA_TYPE}
|
||||||
|
parsedQuery={parsedQuery}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.CLIPBOARD}
|
icon={IconNames.CLIPBOARD}
|
||||||
text={`Copy: ${columnData.COLUMN_NAME}`}
|
text={`Copy: ${columnData.COLUMN_NAME}`}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ResizeEntry } from '@blueprintjs/core';
|
|
||||||
import { ResizeSensor2 } from '@blueprintjs/popover2';
|
import { ResizeSensor2 } from '@blueprintjs/popover2';
|
||||||
import { C, T } from '@druid-toolkit/query';
|
import { C, T } from '@druid-toolkit/query';
|
||||||
import type { Ace } from 'ace-builds';
|
import type { Ace } from 'ace-builds';
|
||||||
|
@ -270,7 +269,7 @@ export class FlexibleQueryInput extends React.PureComponent<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly handleAceContainerResize = (entries: ResizeEntry[]) => {
|
private readonly handleAceContainerResize = (entries: ResizeObserverEntry[]) => {
|
||||||
if (entries.length !== 1) return;
|
if (entries.length !== 1) return;
|
||||||
this.setState({ editorHeight: entries[0].contentRect.height });
|
this.setState({ editorHeight: entries[0].contentRect.height });
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue