diff --git a/web-console/src/bootstrap/react-table-defaults.tsx b/web-console/src/bootstrap/react-table-defaults.tsx index 42039b36da8..531bc8127bc 100644 --- a/web-console/src/bootstrap/react-table-defaults.tsx +++ b/web-console/src/bootstrap/react-table-defaults.tsx @@ -40,7 +40,12 @@ export function bootstrapReactTable() { className: DEFAULT_TABLE_CLASS_NAME, defaultFilterMethod: (filter: Filter, row: any) => { 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, loadingText: '', diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 150e94f2abe..3fc9e1ad87f 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import type { IResizeEntry } from '@blueprintjs/core'; import { FormGroup, HTMLSelect, Radio, RadioGroup, ResizeSensor } from '@blueprintjs/core'; import type { AxisScale } from 'd3-axis'; 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; this.setState({ chartWidth: chartRect.width, diff --git a/web-console/src/react-table/react-table-utils.ts b/web-console/src/react-table/react-table-utils.ts index e1ae5b9b151..91248c55209 100644 --- a/web-console/src/react-table/react-table-utils.ts +++ b/web-console/src/react-table/react-table-utils.ts @@ -107,7 +107,7 @@ export function addOrUpdateFilter(filters: readonly Filter[], filter: Filter): F 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; const modeAndNeedle = parseFilterModeAndNeedle(filter); if (!modeAndNeedle) return true; diff --git a/web-console/src/views/workbench-view/column-tree/__snapshots__/column-tree.spec.tsx.snap b/web-console/src/views/workbench-view/column-tree/__snapshots__/column-tree.spec.tsx.snap index f1ed7216981..93e3c83d041 100644 --- a/web-console/src/views/workbench-view/column-tree/__snapshots__/column-tree.spec.tsx.snap +++ b/web-console/src/views/workbench-view/column-tree/__snapshots__/column-tree.spec.tsx.snap @@ -32,7 +32,13 @@ exports[`ColumnTree matches snapshot 1`] = ` Object { "childNodes": Array [ Object { - "icon": "time", + "icon": , "id": "__time", "label": , }, Object { - "icon": "numerical", + "icon": , "id": "added", "label": , }, Object { - "icon": "floating-point", + "icon": , "id": "addedBy10", "label": +
  • + + + + + + +
    + Aggregate +
    + +
    +
    +
    +
  • + +`; diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.spec.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.spec.tsx new file mode 100644 index 00000000000..a7d7fdcf2c8 --- /dev/null +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.spec.tsx @@ -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 = ( + {}} + /> + ); + + const { container } = render(numberMenu); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.tsx new file mode 100644 index 00000000000..fa172262f07 --- /dev/null +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/complex-menu-items/complex-menu-items.tsx @@ -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 = { + 'COMPLEX': 'APPROX_COUNT_DISTINCT_BUILTIN', + 'COMPLEX': 'APPROX_COUNT_DISTINCT_DS_THETA', + 'COMPLEX': 'APPROX_COUNT_DISTINCT_DS_HLL', +}; + +const QUANTILE_FUNCTIONS: Record = { + 'COMPLEX': '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 ( + { + onQueryChange(parsedQuery.addSelect(ex.as(alias)), true); + }} + /> + ); + } + + const uniqueFn = UNIQUE_FUNCTIONS[columnType]; + const quantileFn = QUANTILE_FUNCTIONS[columnType]; + if (!uniqueFn && !quantileFn) return; + + return ( + + {uniqueFn && aggregateMenuItem(F(uniqueFn, column), `unique_${columnName}`)} + {quantileFn && ( + <> + {aggregateMenuItem(F(quantileFn, column, 0.5), `median_${columnName}`)} + {aggregateMenuItem(F(quantileFn, column, 0.98), `p98_${columnName}`)} + + )} + + ); + } + + return <>{renderAggregateMenu()}; +}); diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/index.ts b/web-console/src/views/workbench-view/column-tree/column-tree-menu/index.ts index 0af13c5eb68..7fd3dced5c1 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/index.ts +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/index.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +export * from './complex-menu-items/complex-menu-items'; export * from './number-menu-items/number-menu-items'; export * from './string-menu-items/string-menu-items'; export * from './time-menu-items/time-menu-items'; diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx index 9c1242dda59..79fbedd54d9 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx @@ -36,9 +36,10 @@ export interface 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) { return ( {filterMenuItem(column.greaterThan(NINE_THOUSAND))} @@ -60,7 +60,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number } function renderRemoveFilter(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return; return ( @@ -75,7 +74,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number } function renderGroupByMenu(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.hasGroupBy()) return; function groupByMenuItem(ex: SqlExpression, alias?: string) { @@ -95,7 +93,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number ); } - const column = C(columnName); return ( {groupByMenuItem(column)} @@ -105,7 +102,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number } function renderRemoveGroupBy(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName); if (!groupedSelectIndexes.length) return; @@ -121,7 +117,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number } function renderAggregateMenu(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.hasGroupBy()) return; function aggregateMenuItem(ex: SqlExpression, alias: string) { @@ -135,7 +130,6 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number ); } - const column = C(columnName); return ( {aggregateMenuItem(F('SUM', column), `sum_${columnName}`)} diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx index 57600105777..db566765d3d 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx @@ -35,9 +35,10 @@ export interface StringMenuItemsProps { } export const StringMenuItems = React.memo(function StringMenuItems(props: StringMenuItemsProps) { - function renderFilterMenu(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; + const { schema, table, columnName, parsedQuery, onQueryChange } = props; + const column = C(columnName); + function renderFilterMenu(): JSX.Element | undefined { function filterMenuItem(clause: SqlExpression, run = true) { return ( {filterMenuItem(column.isNotNull())} @@ -61,7 +61,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderRemoveFilter(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.getEffectiveWhereExpression().containsColumnName(columnName)) return; return ( @@ -76,7 +75,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderRemoveGroupBy(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; const groupedSelectIndexes = parsedQuery.getGroupedSelectIndexesForColumn(columnName); if (!groupedSelectIndexes.length) return; @@ -92,7 +90,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderGroupByMenu(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.hasGroupBy()) return; function groupByMenuItem(ex: SqlExpression, alias?: string) { @@ -112,7 +109,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String ); } - const column = C(columnName); return ( {groupByMenuItem(column)} @@ -123,7 +119,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderAggregateMenu(): JSX.Element | undefined { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.hasGroupBy()) return; 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 ( {aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)} @@ -152,7 +146,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderJoinMenu(): JSX.Element | undefined { - const { schema, table, columnName, parsedQuery, onQueryChange } = props; if (schema !== 'lookup' || !parsedQuery) return; const firstTableName = parsedQuery.getFirstTableName(); if (!firstTableName) return; @@ -212,7 +205,6 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String } function renderRemoveJoin(): JSX.Element | undefined { - const { schema, parsedQuery, onQueryChange } = props; if (schema !== 'lookup' || !parsedQuery) return; if (!parsedQuery.hasJoin()) return; diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx index 706c1cab506..677667ddc87 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx @@ -113,9 +113,10 @@ export interface 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) { return ( {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 { - const { columnName, parsedQuery, onQueryChange } = props; if (!parsedQuery.hasGroupBy()) return; function aggregateMenuItem(ex: SqlExpression, alias: string) { @@ -243,7 +239,6 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt ); } - const column = C(columnName); return ( {aggregateMenuItem(F.max(column), `max_${columnName}`)} diff --git a/web-console/src/views/workbench-view/column-tree/column-tree.tsx b/web-console/src/views/workbench-view/column-tree/column-tree.tsx index e19c2417a49..eee22449c9b 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree.tsx @@ -17,7 +17,7 @@ */ 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 { Popover2 } from '@blueprintjs/popover2'; import type { SqlExpression } from '@druid-toolkit/query'; @@ -39,7 +39,12 @@ import { Deferred, Loader } from '../../../components'; import type { ColumnMetadata } 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'; @@ -400,7 +405,15 @@ export class ColumnTree extends React.PureComponent ({ id: columnData.COLUMN_NAME, - icon: dataTypeToIcon(columnData.DATA_TYPE), + icon: ( + + ), label: ( )} + {parsedQuery && columnData.DATA_TYPE.startsWith('COMPLEX<') && ( + + )} { + private readonly handleAceContainerResize = (entries: ResizeObserverEntry[]) => { if (entries.length !== 1) return; this.setState({ editorHeight: entries[0].contentRect.height }); };