diff --git a/web-console/script/druid b/web-console/script/druid index 122febaf0490..e7e575a5bb06 100755 --- a/web-console/script/druid +++ b/web-console/script/druid @@ -67,6 +67,7 @@ function _build_distribution() { && echo -e "\n\ndruid.extensions.loadList=[\"druid-hdfs-storage\", \"druid-kafka-indexing-service\", \"druid-multi-stage-query\", \"druid-testing-tools\", \"druid-bloom-filter\", \"druid-datasketches\", \"druid-histogram\", \"druid-stats\", \"druid-compressed-bigdecimal\", \"druid-parquet-extensions\", \"druid-deltalake-extensions\"]" >> conf/druid/auto/_common/common.runtime.properties \ && echo -e "\n\ndruid.server.http.allowedHttpMethods=[\"HEAD\"]" >> conf/druid/auto/_common/common.runtime.properties \ && echo -e "\n\ndruid.export.storage.baseDir=/" >> conf/druid/auto/_common/common.runtime.properties \ + && echo -e "\n\ndruid.msq.dart.enabled=true" >> conf/druid/auto/_common/common.runtime.properties \ ) } diff --git a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap index 9aac8421b3f4..1ea3665aed13 100644 --- a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap +++ b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap @@ -213,6 +213,7 @@ exports[`HeaderBar matches snapshot 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", diff --git a/web-console/src/druid-models/dart/dart-query-entry.mock.ts b/web-console/src/druid-models/dart/dart-query-entry.mock.ts new file mode 100644 index 000000000000..f2409abb0cb1 --- /dev/null +++ b/web-console/src/druid-models/dart/dart-query-entry.mock.ts @@ -0,0 +1,49 @@ +/* + * 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 type { DartQueryEntry } from './dart-query-entry'; + +export const DART_QUERIES: DartQueryEntry[] = [ + { + sqlQueryId: '77b2344c-0a1f-4aa0-b127-de6fbc0c2b57', + dartQueryId: '99cdba0d-ed77-433d-9adc-0562d816e105', + sql: 'SELECT\n "URL",\n COUNT(*)\nFROM "c"\nGROUP BY 1\nORDER BY 2 DESC\nLIMIT 50\n', + authenticator: 'allowAll', + identity: 'allowAll', + startTime: '2024-09-28T07:41:21.194Z', + state: 'RUNNING', + }, + { + sqlQueryId: '45441cf5-d8b7-46cb-b6d8-682334f056ef', + dartQueryId: '25af9bff-004d-494e-b562-2752dc3779c8', + sql: 'SELECT\n "URL",\n COUNT(*)\nFROM "c"\nGROUP BY 1\nORDER BY 2 DESC\nLIMIT 50\n', + authenticator: 'allowAll', + identity: 'allowAll', + startTime: '2024-09-28T07:41:22.854Z', + state: 'CANCELED', + }, + { + sqlQueryId: 'f7257c78-6bbe-439d-99ba-f4998b300770', + dartQueryId: 'f7c2d644-9c40-4d61-9fdb-7b0e15219886', + sql: 'SELECT\n "URL",\n COUNT(*)\nFROM "c"\nGROUP BY 1\nORDER BY 2 DESC\nLIMIT 50\n', + authenticator: 'allowAll', + identity: 'allowAll', + startTime: '2024-09-28T07:41:24.425Z', + state: 'ACCEPTED', + }, +]; diff --git a/web-console/src/druid-models/dart/dart-query-entry.ts b/web-console/src/druid-models/dart/dart-query-entry.ts new file mode 100644 index 000000000000..472248b881e9 --- /dev/null +++ b/web-console/src/druid-models/dart/dart-query-entry.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export interface DartQueryEntry { + sqlQueryId: string; + dartQueryId: string; + sql: string; + authenticator: string; + identity: string; + startTime: string; + state: 'ACCEPTED' | 'RUNNING' | 'CANCELED'; +} diff --git a/web-console/src/druid-models/druid-engine/druid-engine.ts b/web-console/src/druid-models/druid-engine/druid-engine.ts index f1942e50c543..335d22e96c00 100644 --- a/web-console/src/druid-models/druid-engine/druid-engine.ts +++ b/web-console/src/druid-models/druid-engine/druid-engine.ts @@ -16,9 +16,14 @@ * limitations under the License. */ -export type DruidEngine = 'native' | 'sql-native' | 'sql-msq-task'; +export type DruidEngine = 'native' | 'sql-native' | 'sql-msq-task' | 'sql-msq-dart'; -export const DRUID_ENGINES: DruidEngine[] = ['native', 'sql-native', 'sql-msq-task']; +export const DRUID_ENGINES: DruidEngine[] = [ + 'native', + 'sql-native', + 'sql-msq-task', + 'sql-msq-dart', +]; export function validDruidEngine( possibleDruidEngine: string | undefined, diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts index e768afeb4b9c..dfeeeeaac837 100644 --- a/web-console/src/druid-models/index.ts +++ b/web-console/src/druid-models/index.ts @@ -20,6 +20,7 @@ export * from './async-query/async-query'; export * from './compaction-config/compaction-config'; export * from './compaction-status/compaction-status'; export * from './coordinator-dynamic-config/coordinator-dynamic-config'; +export * from './dart/dart-query-entry'; export * from './dimension-spec/dimension-spec'; export * from './druid-engine/druid-engine'; export * from './execution/execution'; diff --git a/web-console/src/druid-models/stages/stages.ts b/web-console/src/druid-models/stages/stages.ts index fbb2c1cd3d46..ddddeab5d2fb 100644 --- a/web-console/src/druid-models/stages/stages.ts +++ b/web-console/src/druid-models/stages/stages.ts @@ -18,6 +18,7 @@ import { max, sum } from 'd3-array'; +import { AutoForm } from '../../components'; import { countBy, deleteKeys, filterMap, groupByAsMap, oneOf, zeroDivide } from '../../utils'; import type { InputFormat } from '../input-format/input-format'; import type { InputSource } from '../input-source/input-source'; @@ -252,26 +253,16 @@ export const CPUS_COUNTER_FIELDS: CpusCounterFields[] = [ export function cpusCounterFieldTitle(k: CpusCounterFields) { switch (k) { - case 'main': - return 'Main'; - case 'collectKeyStatistics': return 'Collect key stats'; - case 'mergeInput': - return 'Merge input'; - - case 'hashPartitionOutput': - return 'Hash partition out'; - - case 'mixOutput': - return 'Mix output'; - - case 'sortOutput': - return 'Sort output'; - default: - return k; + // main + // mergeInput + // hashPartitionOutput + // mixOutput + // sortOutput + return AutoForm.makeLabelName(k); } } diff --git a/web-console/src/druid-models/workbench-query/workbench-query.ts b/web-console/src/druid-models/workbench-query/workbench-query.ts index dd75c94b75e7..716fe573a064 100644 --- a/web-console/src/druid-models/workbench-query/workbench-query.ts +++ b/web-console/src/druid-models/workbench-query/workbench-query.ts @@ -528,7 +528,7 @@ export class WorkbenchQuery { }; let cancelQueryId: string | undefined; - if (engine === 'sql-native') { + if (engine === 'sql-native' || engine === 'sql-msq-dart') { cancelQueryId = apiQuery.context.sqlQueryId; if (!cancelQueryId) { // If the sqlQueryId is not explicitly set on the context generate one, so it is possible to cancel the query. @@ -550,6 +550,10 @@ export class WorkbenchQuery { apiQuery.context.sqlStringifyArrays ??= false; } + if (engine === 'sql-msq-dart') { + apiQuery.context.fullReport ??= true; + } + if (Array.isArray(queryParameters) && queryParameters.length) { apiQuery.parameters = queryParameters; } diff --git a/web-console/src/helpers/capabilities.ts b/web-console/src/helpers/capabilities.ts index fe125b67231a..013f9368c58c 100644 --- a/web-console/src/helpers/capabilities.ts +++ b/web-console/src/helpers/capabilities.ts @@ -37,6 +37,7 @@ export type QueryType = 'none' | 'nativeOnly' | 'nativeAndSql'; export interface CapabilitiesValue { queryType: QueryType; multiStageQueryTask: boolean; + multiStageQueryDart: boolean; coordinator: boolean; overlord: boolean; maxTaskSlots?: number; @@ -53,6 +54,7 @@ export class Capabilities { private readonly queryType: QueryType; private readonly multiStageQueryTask: boolean; + private readonly multiStageQueryDart: boolean; private readonly coordinator: boolean; private readonly overlord: boolean; private readonly maxTaskSlots?: number; @@ -139,6 +141,15 @@ export class Capabilities { } } + static async detectMultiStageQueryDart(): Promise { + try { + const resp = await Api.instance.get(`/druid/v2/sql/dart/enabled?capabilities`); + return Boolean(resp.data.enabled); + } catch { + return false; + } + } + static async detectCapabilities(): Promise { const queryType = await Capabilities.detectQueryType(); if (typeof queryType === 'undefined') return; @@ -154,11 +165,15 @@ export class Capabilities { coordinator = overlord = await Capabilities.detectManagementProxy(); } - const multiStageQueryTask = await Capabilities.detectMultiStageQueryTask(); + const [multiStageQueryTask, multiStageQueryDart] = await Promise.all([ + Capabilities.detectMultiStageQueryTask(), + Capabilities.detectMultiStageQueryDart(), + ]); return new Capabilities({ queryType, multiStageQueryTask, + multiStageQueryDart, coordinator, overlord, }); @@ -179,6 +194,7 @@ export class Capabilities { constructor(value: CapabilitiesValue) { this.queryType = value.queryType; this.multiStageQueryTask = value.multiStageQueryTask; + this.multiStageQueryDart = value.multiStageQueryDart; this.coordinator = value.coordinator; this.overlord = value.overlord; this.maxTaskSlots = value.maxTaskSlots; @@ -188,6 +204,7 @@ export class Capabilities { return { queryType: this.queryType, multiStageQueryTask: this.multiStageQueryTask, + multiStageQueryDart: this.multiStageQueryDart, coordinator: this.coordinator, overlord: this.overlord, maxTaskSlots: this.maxTaskSlots, @@ -248,6 +265,10 @@ export class Capabilities { return this.multiStageQueryTask; } + public hasMultiStageQueryDart(): boolean { + return this.multiStageQueryDart; + } + public getSupportedQueryEngines(): DruidEngine[] { const queryEngines: DruidEngine[] = ['native']; if (this.hasSql()) { @@ -256,6 +277,9 @@ export class Capabilities { if (this.hasMultiStageQueryTask()) { queryEngines.push('sql-msq-task'); } + if (this.hasMultiStageQueryDart()) { + queryEngines.push('sql-msq-dart'); + } return queryEngines; } @@ -282,36 +306,42 @@ export class Capabilities { Capabilities.FULL = new Capabilities({ queryType: 'nativeAndSql', multiStageQueryTask: true, + multiStageQueryDart: true, coordinator: true, overlord: true, }); Capabilities.NO_SQL = new Capabilities({ queryType: 'nativeOnly', multiStageQueryTask: false, + multiStageQueryDart: false, coordinator: true, overlord: true, }); Capabilities.COORDINATOR_OVERLORD = new Capabilities({ queryType: 'none', multiStageQueryTask: false, + multiStageQueryDart: false, coordinator: true, overlord: true, }); Capabilities.COORDINATOR = new Capabilities({ queryType: 'none', multiStageQueryTask: false, + multiStageQueryDart: false, coordinator: true, overlord: false, }); Capabilities.OVERLORD = new Capabilities({ queryType: 'none', multiStageQueryTask: false, + multiStageQueryDart: false, coordinator: false, overlord: true, }); Capabilities.NO_PROXY = new Capabilities({ queryType: 'nativeAndSql', multiStageQueryTask: true, + multiStageQueryDart: false, coordinator: false, overlord: false, }); diff --git a/web-console/src/utils/druid-query.ts b/web-console/src/utils/druid-query.ts index fba63b946000..d1481366fa78 100644 --- a/web-console/src/utils/druid-query.ts +++ b/web-console/src/utils/druid-query.ts @@ -342,6 +342,19 @@ export async function queryDruidSql( return sqlResultResp.data; } +export async function queryDruidSqlDart( + sqlQueryPayload: Record, + cancelToken?: CancelToken, +): Promise { + let sqlResultResp: AxiosResponse; + try { + sqlResultResp = await Api.instance.post('/druid/v2/sql/dart', sqlQueryPayload, { cancelToken }); + } catch (e) { + throw new Error(getDruidErrorMessage(e)); + } + return sqlResultResp.data; +} + export interface QueryExplanation { query: any; signature: { name: string; type: string }[]; diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx index d4efec06e224..a178fc2c009c 100644 --- a/web-console/src/utils/local-storage-keys.tsx +++ b/web-console/src/utils/local-storage-keys.tsx @@ -53,10 +53,12 @@ export const LocalStorageKeys = { WORKBENCH_PANE_SIZE: 'workbench-pane-size' as const, WORKBENCH_HISTORY: 'workbench-history' as const, WORKBENCH_TASK_PANEL: 'workbench-task-panel' as const, + WORKBENCH_DART_PANEL: 'workbench-dart-panel' as const, SQL_DATA_LOADER_CONTENT: 'sql-data-loader-content' as const, EXPLORE_STATE: 'explore-state' as const, + EXPLORE_STICKY: 'explore-sticky' as const, }; export type LocalStorageKeys = (typeof LocalStorageKeys)[keyof typeof LocalStorageKeys]; diff --git a/web-console/src/utils/table-helpers.ts b/web-console/src/utils/table-helpers.ts index 90df7fa10648..45e8758bf6f8 100644 --- a/web-console/src/utils/table-helpers.ts +++ b/web-console/src/utils/table-helpers.ts @@ -35,6 +35,7 @@ export function changePage(pagination: Pagination, page: number): Pagination { export interface ColumnHint { displayName?: string; group?: string; + hidden?: boolean; expressionForWhere?: SqlExpression; formatter?: (x: any) => string; } diff --git a/web-console/src/views/explore-view/components/control-pane/control-pane.tsx b/web-console/src/views/explore-view/components/control-pane/control-pane.tsx index 7fcc6352f293..faa7576bb67a 100644 --- a/web-console/src/views/explore-view/components/control-pane/control-pane.tsx +++ b/web-console/src/views/explore-view/components/control-pane/control-pane.tsx @@ -194,7 +194,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) { }; return { element: ( - allowReordering values={effectiveValue ? [effectiveValue] : []} onValuesChange={vs => onValueChange(vs[0])} @@ -223,7 +223,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) { ); return { element: ( - allowReordering values={effectiveValue as ExpressionMeta[]} onValuesChange={onValueChange} @@ -266,7 +266,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) { case 'measure': { return { element: ( - values={effectiveValue ? [effectiveValue] : []} onValuesChange={vs => onValueChange(vs[0])} singleton @@ -284,9 +284,11 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) { /> ), onDropColumn: column => { - const measures = Measure.getPossibleMeasuresForColumn(column); - if (!measures.length) return; - onValueChange(measures[0]); + const candidateMeasures = Measure.getPossibleMeasuresForColumn(column).filter( + p => !effectiveValue || effectiveValue.name !== p.name, + ); + if (!candidateMeasures.length) return; + onValueChange(candidateMeasures[0]); }, onDropMeasure: onValueChange, }; @@ -313,11 +315,11 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) { /> ), onDropColumn: column => { - const measures = Measure.getPossibleMeasuresForColumn(column).filter( - p => !effectiveValue.some((v: ExpressionMeta) => v.name === p.name), + const candidateMeasures = Measure.getPossibleMeasuresForColumn(column).filter( + p => !effectiveValue.some((v: Measure) => v.name === p.name), ); - if (!measures.length) return; - onValueChange(effectiveValue.concat(measures[0])); + if (!candidateMeasures.length) return; + onValueChange(effectiveValue.concat(candidateMeasures[0])); }, onDropMeasure: measure => { onValueChange(effectiveValue.concat(measure)); diff --git a/web-console/src/views/explore-view/components/control-pane/named-expressions-input.tsx b/web-console/src/views/explore-view/components/control-pane/named-expressions-input.tsx index ae498341ecd6..d93a6c522cea 100644 --- a/web-console/src/views/explore-view/components/control-pane/named-expressions-input.tsx +++ b/web-console/src/views/explore-view/components/control-pane/named-expressions-input.tsx @@ -61,6 +61,7 @@ export const NamedExpressionsInput = function NamedExpressionsInput< const onDragOver = useCallback( (e: React.DragEvent, i: number) => { + if (dragIndex === -1) return; const targetRect = e.currentTarget.getBoundingClientRect(); const before = e.clientX - targetRect.left <= targetRect.width / 2; setDropBefore(before); diff --git a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx index 353179c905e4..8751aad7e600 100644 --- a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx +++ b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx @@ -52,6 +52,7 @@ export const ContainsFilterControl = React.memo(function ContainsFilterControl( ), ) .changeOrderByExpression(F.count().toOrderByExpression('DESC')) + .changeLimitValue(101) .toString(), // eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 'makePattern' from deps [querySource.query, filter, column, contains, negated], diff --git a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx index ea6bdf743858..dd9a90d4bf6f 100644 --- a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx +++ b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx @@ -58,6 +58,7 @@ export const RegexpFilterControl = React.memo(function RegexpFilterControl( SqlExpression.and(filter, regexp ? filterPatternToExpression(filterPattern) : undefined), ) .changeOrderByExpression(F.count().toOrderByExpression('DESC')) + .changeLimitValue(101) .toString(), // eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 'makePattern' from deps [querySource.query, filter, column, regexp, negated], diff --git a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx index a15d3daad762..d0d5f3f460a6 100644 --- a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx +++ b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx @@ -16,14 +16,15 @@ * limitations under the License. */ -import { FormGroup, InputGroup, Menu, MenuItem } from '@blueprintjs/core'; +import { FormGroup, Menu, MenuItem } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import type { QueryResult, SqlQuery, ValuesFilterPattern } from '@druid-toolkit/query'; -import { C, F, L, SqlExpression, SqlLiteral } from '@druid-toolkit/query'; +import type { QueryResult, ValuesFilterPattern } from '@druid-toolkit/query'; +import { C, F, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import React, { useMemo, useState } from 'react'; +import { ClearableInput } from '../../../../../../components'; import { useQueryManager } from '../../../../../../hooks'; -import { caseInsensitiveContains } from '../../../../../../utils'; +import { caseInsensitiveContains, filterMap } from '../../../../../../utils'; import type { QuerySource } from '../../../../models'; import { toggle } from '../../../../utils'; import { ColumnValue } from '../../column-value/column-value'; @@ -46,21 +47,21 @@ export const ValuesFilterControl = React.memo(function ValuesFilterControl( const [initValues] = useState(selectedValues); const [searchString, setSearchString] = useState(''); - const valuesQuery = useMemo(() => { - const columnRef = C(column); - const queryParts: string[] = [`SELECT ${columnRef.as('c')}`, `FROM (${querySource.query})`]; - - const filterEx = SqlExpression.and( - filter, - searchString ? F('ICONTAINS_STRING', columnRef, L(searchString)) : undefined, - ); - if (!(filterEx instanceof SqlLiteral)) { - queryParts.push(`WHERE ${filterEx}`); - } - - queryParts.push(`GROUP BY 1 ORDER BY COUNT(*) DESC LIMIT 101`); - return queryParts.join('\n'); - }, [querySource.query, filter, column, searchString]); + const valuesQuery = useMemo( + () => + SqlQuery.from(querySource.query) + .addSelect(C(column).as('c'), { addToGroupBy: 'end' }) + .changeWhereExpression( + SqlExpression.and( + filter, + searchString ? F('ICONTAINS_STRING', C(column), searchString) : undefined, + ), + ) + .changeOrderByExpression(F.count().toOrderByExpression('DESC')) + .changeLimitValue(101) + .toString(), + [querySource.query, filter, column, searchString], + ); const [valuesState] = useQueryManager({ query: valuesQuery, @@ -77,42 +78,37 @@ export const ValuesFilterControl = React.memo(function ValuesFilterControl( if (values) { valuesToShow = valuesToShow.concat(values.filter(v => !initValues.includes(v))); } - if (searchString) { - valuesToShow = valuesToShow.filter(v => caseInsensitiveContains(v, searchString)); - } const showSearch = querySource.columns.find(c => c.name === column)?.sqlType !== 'BOOLEAN'; - return ( {showSearch && ( - setSearchString(e.target.value)} - placeholder="Search" - /> + )} - {valuesToShow.map((v, i) => ( - } - shouldDismissPopover={false} - onClick={e => { - setFilterPattern({ - ...filterPattern, - values: e.altKey ? [v] : toggle(selectedValues, v), - }); - }} - /> - ))} + {filterMap(valuesToShow, (v, i) => { + if (!caseInsensitiveContains(v, searchString)) return; + return ( + } + shouldDismissPopover={false} + onClick={e => { + setFilterPattern({ + ...filterPattern, + values: e.altKey ? [v] : toggle(selectedValues, v), + }); + }} + /> + ); + })} {valuesState.loading && } diff --git a/web-console/src/views/explore-view/components/generic-output-table/generic-output-table.tsx b/web-console/src/views/explore-view/components/generic-output-table/generic-output-table.tsx index 0050a34122c6..b557d99b4fde 100644 --- a/web-console/src/views/explore-view/components/generic-output-table/generic-output-table.tsx +++ b/web-console/src/views/explore-view/components/generic-output-table/generic-output-table.tsx @@ -428,6 +428,7 @@ export const GenericOutputTable = React.memo(function GenericOutputTable( columns={columnNester( queryResult.header.map((column, i) => { const h = column.name; + const hint = columnHints?.get(h); const icon = showTypeIcons ? columnToIcon(column) : undefined; return { @@ -446,9 +447,10 @@ export const GenericOutputTable = React.memo(function GenericOutputTable( }, headerClassName: getHeaderClassName(h), accessor: String(i), + show: !hint?.hidden, Cell(row) { const value = row.value; - const formatter = columnHints?.get(h)?.formatter || formatNumber; + const formatter = hint?.formatter || formatNumber; return (
getCellMenu(column, i, value)} />}> diff --git a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx index 4cc4fcd674ce..0e4cc3cb77af 100644 --- a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx +++ b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx @@ -48,9 +48,8 @@ export const ColumnDialog = React.memo(function ColumnDialog(props: ColumnDialog if (!expression) return; return SqlQuery.from(QuerySource.stripToBaseSource(querySource.query)) .addSelect(F.cast(expression, 'VARCHAR').as('v'), { addToGroupBy: 'end' }) - .applyIf( - querySource.baseColumns.find(column => column.isTimeColumn()), - q => q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`), + .applyIf(querySource.hasBaseTimeColumn(), q => + q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`), ) .changeLimitValue(100) .toString(); @@ -151,7 +150,7 @@ export const ColumnDialog = React.memo(function ColumnDialog(props: ColumnDialog } else { onApply( querySource.changeColumn(initExpressionName, newExpression), - new Map([[initExpression.getOutputName()!, newExpression.getOutputName()!]]), + new Map([[initExpressionName, newExpression.getOutputName()!]]), ); } } else { diff --git a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx index f02facf61f09..b48638fe72f5 100644 --- a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx +++ b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx @@ -57,9 +57,8 @@ export const MeasureDialog = React.memo(function MeasureDialog(props: MeasureDia .changeWithParts([SqlWithPart.simple('t', QuerySource.stripToBaseSource(querySource.query))]) .addSelect(L('Overall').as('label')) .addSelect(expression.as('value')) - .applyIf( - querySource.baseColumns.find(column => column.isTimeColumn()), - q => q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`), + .applyIf(querySource.hasBaseTimeColumn(), q => + q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`), ) .toString(); }, [querySource.query, formula]); diff --git a/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.scss b/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.scss new file mode 100644 index 000000000000..ef7650c1f655 --- /dev/null +++ b/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.scss @@ -0,0 +1,44 @@ +/* + * 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 '../../../../../variables'; + +.nested-column-dialog { + &.#{$bp-ns}-dialog { + width: 50vw; + min-height: 540px; + } + + .#{$bp-ns}-dialog-body { + display: flex; + flex-direction: column; + + .path-selector { + flex: 1; + padding: 5px 0; + height: 400px; + overflow: auto; + border-left: 1px solid rgba(15, 19, 32, 0.4); + border-right: 1px solid rgba(15, 19, 32, 0.4); + } + } + + .#{$bp-ns}-dialog-footer { + margin-top: 0; + } +} diff --git a/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.tsx b/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.tsx new file mode 100644 index 000000000000..8ec09085f22e --- /dev/null +++ b/web-console/src/views/explore-view/components/resource-pane/nested-column-dialog/nested-column-dialog.tsx @@ -0,0 +1,176 @@ +/* + * 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 { + Button, + ButtonGroup, + Classes, + Dialog, + FormGroup, + InputGroup, + Intent, + Menu, + Tag, +} from '@blueprintjs/core'; +import type { SqlExpression } from '@druid-toolkit/query'; +import { type QueryResult, F, sql, SqlFunction, SqlQuery } from '@druid-toolkit/query'; +import React, { useState } from 'react'; + +import { ClearableInput, Loader, MenuCheckbox } from '../../../../../components'; +import { useQueryManager } from '../../../../../hooks'; +import { caseInsensitiveContains, filterMap, pluralIfNeeded } from '../../../../../utils'; +import { ExpressionMeta, QuerySource } from '../../../models'; +import { toggle } from '../../../utils'; + +import './nested-column-dialog.scss'; + +const ARRAY_CONCAT_AGG_SIZE = 10000; + +export interface NestedColumnDialogProps { + nestedColumn: SqlExpression; + onApply(newQuery: SqlQuery): void; + querySource: QuerySource; + runSqlQuery(query: string | SqlQuery): Promise; + onClose(): void; +} + +export const NestedColumnDialog = React.memo(function NestedColumnDialog( + props: NestedColumnDialogProps, +) { + const { nestedColumn, onApply, querySource, runSqlQuery, onClose } = props; + const [searchString, setSearchString] = useState(''); + const [selectedPaths, setSelectedPaths] = useState([]); + const [namingScheme, setNamingScheme] = useState(`${nestedColumn.getFirstColumnName()}[%]`); + + const [pathsState] = useQueryManager({ + query: nestedColumn, + processQuery: async nestedColumn => { + const query = SqlQuery.from(QuerySource.stripToBaseSource(querySource.query)) + .addSelect( + SqlFunction.decorated('ARRAY_CONCAT_AGG', 'DISTINCT', [ + F('JSON_PATHS', nestedColumn), + ARRAY_CONCAT_AGG_SIZE, + ]), + ) + .applyIf(querySource.hasBaseTimeColumn(), q => + q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`), + ); + + const pathResult = await runSqlQuery(query); + + const paths = pathResult.rows[0]?.[0]; + if (!Array.isArray(paths)) throw new Error('Could not get paths'); + + return paths; + }, + }); + + const paths = pathsState.data; + return ( + +
+

+ Replace {String(nestedColumn.getOutputName())} with path expansions for + the selected paths. +

+ {pathsState.isLoading() && } + {pathsState.getErrorMessage()} + {paths && ( + + + + {filterMap(paths, (path, i) => { + if (!caseInsensitiveContains(path, searchString)) return; + return ( + setSelectedPaths(toggle(selectedPaths, path))} + text={path} + /> + ); + })} + + +
+
+
+
+
+
+
+
+ ); +}); diff --git a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx index f6489eb17e2c..2de75e32d86e 100644 --- a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx +++ b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx @@ -40,6 +40,7 @@ import type { Rename } from '../../utils'; import { ColumnDialog } from './column-dialog/column-dialog'; import { MeasureDialog } from './measure-dialog/measure-dialog'; +import { NestedColumnDialog } from './nested-column-dialog/nested-column-dialog'; import './resource-pane.scss'; @@ -67,6 +68,9 @@ export const ResourcePane = function ResourcePane(props: ResourcePaneProps) { const [columnSearch, setColumnSearch] = useState(''); const [columnEditorOpenOn, setColumnEditorOpenOn] = useState(); + const [nestedColumnEditorOpenOn, setNestedColumnEditorOpenOn] = useState< + SqlExpression | undefined + >(); const [measureEditorOpenOn, setMeasureEditorOpenOn] = useState(); function applyUtil(nameTransform: (columnName: string) => string) { @@ -112,6 +116,7 @@ export const ResourcePane = function ResourcePane(props: ResourcePaneProps) {
{filterMap(querySource.columns, (column, i) => { const columnName = column.name; + const isNestedColumn = column.nativeType === 'COMPLEX'; if (!caseInsensitiveContains(columnName, columnSearch)) return; return ( - {onFilter && ( + {isNestedColumn ? ( onFilter(column)} + icon={IconNames.EXPAND_ALL} + text="Expand nested column" + onClick={() => + setNestedColumnEditorOpenOn( + querySource.getSourceExpressionForColumn(columnName), + ) + } /> + ) : ( + <> + {onFilter && ( + onFilter(column)} + /> + )} + onShowColumn(column)} + /> + + )} - onShowColumn(column)} - /> -
{ e.dataTransfer.effectAllowed = 'all'; DragHelper.dragColumn = column; @@ -268,6 +287,15 @@ export const ResourcePane = function ResourcePane(props: ResourcePaneProps) { onClose={() => setColumnEditorOpenOn(undefined)} /> )} + {nestedColumnEditorOpenOn && ( + onQueryChange(newQuery, undefined)} + querySource={querySource} + runSqlQuery={runSqlQuery} + onClose={() => setNestedColumnEditorOpenOn(undefined)} + /> + )} {measureEditorOpenOn && ( MAX_PAST_QUERIES) QUERY_HISTORY.pop(); -} - -function getFormattedQueryHistory(): string { - return QUERY_HISTORY.map( - ({ time, sqlQuery }) => `At ${time.toISOString()} ran query:\n\n${sqlQuery}`, - ).join('\n\n-----------------------------------------------------\n\n'); +function getStickyParameterValuesForModule(moduleId: string): ParameterValues { + return localStorageGetJson(LocalStorageKeys.EXPLORE_STICKY)?.[moduleId] || {}; } // --------------------------------------- @@ -81,7 +74,7 @@ const queryRunner = new QueryRunner({ inflateDateStrategy: 'fromSqlTypes', executor: async (sqlQueryPayload, isSql, cancelToken) => { if (!isSql) throw new Error('should never get here'); - addQueryToHistory(sqlQueryPayload.query); + QUERY_LOG.addQuery(sqlQueryPayload.query); return Api.instance.post('/druid/v2/sql', sqlQueryPayload, { cancelToken }); }, }); @@ -90,6 +83,9 @@ async function runSqlQuery(query: string | SqlQuery): Promise { try { return await queryRunner.runQuery({ query, + defaultQueryContext: { + sqlStringifyArrays: false, + }, }); } catch (e) { throw new DruidError(e); @@ -193,10 +189,25 @@ export const ExploreView = React.memo(function ExploreView() { } function resetParameterValues() { - setParameterValues({}); + setParameterValues(getStickyParameterValuesForModule(moduleId)); } function updateParameterValues(newParameterValues: ParameterValues) { + // Evaluate sticky-ness + if (module) { + const currentExploreSticky = localStorageGetJson(LocalStorageKeys.EXPLORE_STICKY) || {}; + const currentModuleSticky = currentExploreSticky[moduleId] || {}; + const newModuleSticky = { + ...currentModuleSticky, + ...mapRecord(newParameterValues, (v, k) => (module.parameters[k]?.sticky ? v : undefined)), + }; + + localStorageSetJson(LocalStorageKeys.EXPLORE_STICKY, { + ...currentExploreSticky, + [moduleId]: isEmpty(newModuleSticky) ? undefined : newModuleSticky, + }); + } + setParameterValues({ ...parameterValues, ...newParameterValues }); } @@ -311,7 +322,8 @@ export const ExploreView = React.memo(function ExploreView() { ]} selectedModuleId={moduleId} onSelectedModuleIdChange={newModuleId => { - const newParameterValues: ParameterValues = {}; + const newParameterValues = getStickyParameterValuesForModule(newModuleId); + const oldModule = ModuleRepository.getModule(moduleId); const newModule = ModuleRepository.getModule(newModuleId); if (oldModule && newModule) { @@ -349,9 +361,9 @@ export const ExploreView = React.memo(function ExploreView() { { - copy(QUERY_HISTORY[0]?.sqlQuery, { format: 'text/plain' }); + copy(QUERY_LOG.getLastQuery()!, { format: 'text/plain' }); AppToaster.show({ message: `Copied query to clipboard`, intent: Intent.SUCCESS, @@ -360,9 +372,9 @@ export const ExploreView = React.memo(function ExploreView() { /> { - setShownText(getFormattedQueryHistory()); + setShownText(QUERY_LOG.getFormatted()); }} /> ': return [ diff --git a/web-console/src/views/explore-view/models/parameter.ts b/web-console/src/views/explore-view/models/parameter.ts index b7a952a14006..f4a5f622d737 100644 --- a/web-console/src/views/explore-view/models/parameter.ts +++ b/web-console/src/views/explore-view/models/parameter.ts @@ -85,6 +85,7 @@ export type TypedParameterDefinition = TypedE | ParameterTypes[Type] | ((querySource: QuerySource) => ParameterTypes[Type] | undefined); + sticky?: boolean; required?: ModuleFunctor; description?: ModuleFunctor; placeholder?: string; diff --git a/web-console/src/views/explore-view/models/query-source.ts b/web-console/src/views/explore-view/models/query-source.ts index a8a5257e311e..4ff83cf14d65 100644 --- a/web-console/src/views/explore-view/models/query-source.ts +++ b/web-console/src/views/explore-view/models/query-source.ts @@ -95,7 +95,9 @@ export class QuerySource { let effectiveColumns = columns; if (query.getSelectExpressionsArray().some(ex => ex instanceof SqlStar)) { // The query has a star so carefully pick the columns that make sense - effectiveColumns = columns.filter(c => c.sqlType !== 'OTHER'); + effectiveColumns = columns.filter( + c => c.sqlType !== 'OTHER' || c.nativeType === 'COMPLEX', + ); } let measures = Measure.extractQueryMeasures(query); @@ -179,6 +181,10 @@ export class QuerySource { return this.measures.some(m => m.name === name); } + public hasBaseTimeColumn(): boolean { + return this.baseColumns.some(column => column.isTimeColumn()); + } + public getSourceExpressionForColumn(outputName: string): SqlExpression { const selectExpressionsArray = this.query.getSelectExpressionsArray(); @@ -224,12 +230,12 @@ export class QuerySource { return noStarQuery.addSelect(newExpression); } - public addColumnAfter(neighborName: string, newExpression: SqlExpression): SqlQuery { + public addColumnAfter(neighborName: string, ...newExpressions: SqlExpression[]): SqlQuery { const noStarQuery = QuerySource.materializeStarIfNeeded(this.query, this.columns); return noStarQuery.changeSelectExpressions( noStarQuery .getSelectExpressionsArray() - .flatMap(ex => (ex.getOutputName() === neighborName ? [ex, newExpression] : ex)), + .flatMap(ex => (ex.getOutputName() === neighborName ? [ex, ...newExpressions] : ex)), ); } diff --git a/web-console/src/views/explore-view/modules/grouping-table-module.tsx b/web-console/src/views/explore-view/modules/grouping-table-module.tsx index c8aa74922eca..e2cba7cef6d7 100644 --- a/web-console/src/views/explore-view/modules/grouping-table-module.tsx +++ b/web-console/src/views/explore-view/modules/grouping-table-module.tsx @@ -105,6 +105,7 @@ ModuleRepository.registerModule({ count: `Show ' values'`, }, defaultValue: 'null', + sticky: true, visible: ({ parameterValues }) => Boolean((parameterValues.showColumns || []).length), }, pivotColumn: { diff --git a/web-console/src/views/explore-view/modules/record-table-module.tsx b/web-console/src/views/explore-view/modules/record-table-module.tsx index b272a4dfabe1..38e2cfb6fac3 100644 --- a/web-console/src/views/explore-view/modules/record-table-module.tsx +++ b/web-console/src/views/explore-view/modules/record-table-module.tsx @@ -21,10 +21,9 @@ import React, { useMemo } from 'react'; import { Loader } from '../../../components'; import { useQueryManager } from '../../../hooks'; -import { - calculateInitPageSize, - GenericOutputTable, -} from '../components/generic-output-table/generic-output-table'; +import type { ColumnHint } from '../../../utils'; +import { filterMap } from '../../../utils'; +import { calculateInitPageSize, GenericOutputTable } from '../components'; import { ModuleRepository } from '../module-repository/module-repository'; import './record-table-module.scss'; @@ -33,6 +32,7 @@ interface RecordTableParameterValues { maxRows: number; ascending: boolean; showTypeIcons: boolean; + hideNullColumns: boolean; } ModuleRepository.registerModule({ @@ -50,10 +50,18 @@ ModuleRepository.registerModule({ ascending: { type: 'boolean', defaultValue: false, + sticky: true, }, showTypeIcons: { type: 'boolean', defaultValue: true, + sticky: true, + }, + hideNullColumns: { + type: 'boolean', + label: 'Hide all null columns', + defaultValue: false, + sticky: true, }, }, component: function RecordTableModule(props) { @@ -77,6 +85,18 @@ ModuleRepository.registerModule({ }); const resultData = resultState.getSomeData(); + + let columnHints: Map | undefined; + if (parameterValues.hideNullColumns && resultData) { + columnHints = new Map( + filterMap(resultData.header, (column, i) => + resultData.getColumnByIndex(i)?.every(v => v == null) + ? [column.name, { hidden: true }] + : undefined, + ), + ); + } + return (
{resultState.error ? ( @@ -84,6 +104,7 @@ ModuleRepository.registerModule({ ) : resultData ? ( ({ }, snappyHighlight: { type: 'boolean', - label: 'Snap highlight to nearest dates', + label: 'Snap highlight to granularity', defaultValue: true, + sticky: true, }, }, component: function TimeChartModule(props) { diff --git a/web-console/src/views/explore-view/query-macros/aggregate.ts b/web-console/src/views/explore-view/query-macros/aggregate.ts index 9a9b9211eb02..377c7f5b02d0 100644 --- a/web-console/src/views/explore-view/query-macros/aggregate.ts +++ b/web-console/src/views/explore-view/query-macros/aggregate.ts @@ -20,6 +20,7 @@ import { C, SqlFunction, SqlQuery } from '@druid-toolkit/query'; import { filterMap, uniq } from '../../../utils'; import { Measure } from '../models'; +import { KNOWN_AGGREGATIONS } from '../utils'; export function rewriteAggregate(query: SqlQuery, measures: Measure[]): SqlQuery { const usedMeasures: Map = new Map(); @@ -35,7 +36,14 @@ export function rewriteAggregate(query: SqlQuery, measures: Measure[]): SqlQuery if (!measure) throw new Error(`${Measure.AGGREGATE} of unknown measure '${measureName}'`); usedMeasures.set(measureName, true); - return measure.expression; + + let measureExpression = measure.expression; + const filter = ex.getWhereExpression(); + if (filter) { + measureExpression = measureExpression.addFilterToAggregations(filter, KNOWN_AGGREGATIONS); + } + + return measureExpression; } // If we encounter a (the) query with the measure definitions, and we have used those measures then expand out all the columns within them diff --git a/web-console/src/views/explore-view/utils/index.ts b/web-console/src/views/explore-view/utils/index.ts index 215b4c9d44da..8469c83b9279 100644 --- a/web-console/src/views/explore-view/utils/index.ts +++ b/web-console/src/views/explore-view/utils/index.ts @@ -21,8 +21,10 @@ export * from './duration'; export * from './filter-pattern-helpers'; export * from './general'; export * from './get-auto-granularity'; +export * from './known-aggregations'; export * from './max-time-for-table'; export * from './misc'; +export * from './query-log'; export * from './snap-to-granularity'; export * from './table-query'; export * from './time-manipulation'; diff --git a/web-console/src/views/explore-view/utils/known-aggregations.ts b/web-console/src/views/explore-view/utils/known-aggregations.ts new file mode 100644 index 000000000000..0df2420648c6 --- /dev/null +++ b/web-console/src/views/explore-view/utils/known-aggregations.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +export const KNOWN_AGGREGATIONS = [ + 'COUNT', + 'SUM', + 'MIN', + 'MAX', + 'AVG', + 'APPROX_COUNT_DISTINCT', + 'APPROX_COUNT_DISTINCT_BUILTIN', + 'APPROX_QUANTILE', + 'APPROX_QUANTILE_FIXED_BUCKETS', + 'BLOOM_FILTER', + 'VAR_POP', + 'VAR_SAMP', + 'VARIANCE', + 'STDDEV_POP', + 'STDDEV_SAMP', + 'STDDEV', + 'EARLIEST', + 'EARLIEST_BY', + 'LATEST', + 'LATEST_BY', + 'ANY_VALUE', + 'GROUPING', + 'ARRAY_AGG', + 'ARRAY_AGG', + 'ARRAY_CONCAT_AGG', + 'ARRAY_CONCAT_AGG', + 'STRING_AGG', + 'LISTAGG', + 'BIT_AND', + 'BIT_OR', + 'BIT_XOR', + 'APPROX_COUNT_DISTINCT_DS_THETA', + 'DS_THETA', + 'APPROX_QUANTILE_DS', + 'DS_QUANTILES_SKETCH', + 'DS_TUPLE_DOUBLES', + 'DS_TUPLE_DOUBLES', + 'TDIGEST_QUANTILE', + 'TDIGEST_GENERATE_SKETCH', +]; diff --git a/web-console/src/views/explore-view/utils/query-log.ts b/web-console/src/views/explore-view/utils/query-log.ts new file mode 100644 index 000000000000..20bfb6ef0d41 --- /dev/null +++ b/web-console/src/views/explore-view/utils/query-log.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +interface QueryLogEntry { + time: Date; + sqlQuery: string; +} + +const MAX_QUERIES_TO_LOG = 10; + +export class QueryLog { + private readonly queryLog: QueryLogEntry[] = []; + + public length(): number { + return this.queryLog.length; + } + + public addQuery(sqlQuery: string): void { + const { queryLog } = this; + queryLog.unshift({ time: new Date(), sqlQuery }); + while (queryLog.length > MAX_QUERIES_TO_LOG) queryLog.pop(); + } + + public getLastQuery(): string | undefined { + return this.queryLog[0]?.sqlQuery; + } + + public getFormatted(): string { + return this.queryLog + .map(({ time, sqlQuery }) => `At ${time.toISOString()} ran query:\n\n${sqlQuery}`) + .join('\n\n-----------------------------------------------------\n\n'); + } +} diff --git a/web-console/src/views/explore-view/utils/table-query.ts b/web-console/src/views/explore-view/utils/table-query.ts index 136b56449750..e7f1499e5f29 100644 --- a/web-console/src/views/explore-view/utils/table-query.ts +++ b/web-console/src/views/explore-view/utils/table-query.ts @@ -37,6 +37,7 @@ import { Measure } from '../models'; import { formatDuration } from './duration'; import { addTableScope } from './general'; +import { KNOWN_AGGREGATIONS } from './known-aggregations'; import type { Compare } from './time-manipulation'; import { computeWhereForCompares } from './time-manipulation'; @@ -48,35 +49,6 @@ export type CompareType = 'value' | 'delta' | 'absDelta' | 'percent' | 'absPerce export type RestrictTop = 'always' | 'never'; -const KNOWN_AGGREGATIONS = [ - 'COUNT', - 'SUM', - 'MIN', - 'MAX', - 'AVG', - 'APPROX_COUNT_DISTINCT', - 'APPROX_COUNT_DISTINCT_DS_HLL', - 'APPROX_COUNT_DISTINCT_DS_THETA', - 'DS_HLL', - 'DS_THETA', - 'APPROX_QUANTILE', - 'APPROX_QUANTILE_DS', - 'APPROX_QUANTILE_FIXED_BUCKETS', - 'DS_QUANTILES_SKETCH', - 'BLOOM_FILTER', - 'TDIGEST_QUANTILE', - 'TDIGEST_GENERATE_SKETCH', - 'VAR_POP', - 'VAR_SAMP', - 'VARIANCE', - 'STDDEV_POP', - 'STDDEV_SAMP', - 'STDDEV', - 'EARLIEST', - 'LATEST', - 'ANY_VALUE', -]; - const DRUID_DEFAULT_TOTAL_SUB_QUERY_LIMIT = 100000; const COMMON_NAME = 'common'; diff --git a/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap b/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap index 9223fb7eb5c2..02ac85096a65 100644 --- a/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap +++ b/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap @@ -9,6 +9,7 @@ exports[`HomeView matches snapshot (coordinator) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": false, "queryType": "none", @@ -21,6 +22,7 @@ exports[`HomeView matches snapshot (coordinator) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": false, "queryType": "none", @@ -32,6 +34,7 @@ exports[`HomeView matches snapshot (coordinator) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": false, "queryType": "none", @@ -44,6 +47,7 @@ exports[`HomeView matches snapshot (coordinator) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": false, "queryType": "none", @@ -55,6 +59,7 @@ exports[`HomeView matches snapshot (coordinator) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": false, "queryType": "none", @@ -73,6 +78,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -85,6 +91,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -96,6 +103,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -109,6 +117,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -120,6 +129,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -132,6 +142,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -143,6 +154,7 @@ exports[`HomeView matches snapshot (full) 1`] = ` Capabilities { "coordinator": true, "maxTaskSlots": undefined, + "multiStageQueryDart": true, "multiStageQueryTask": true, "overlord": true, "queryType": "nativeAndSql", @@ -161,6 +173,7 @@ exports[`HomeView matches snapshot (overlord) 1`] = ` Capabilities { "coordinator": false, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": true, "queryType": "none", @@ -173,6 +186,7 @@ exports[`HomeView matches snapshot (overlord) 1`] = ` Capabilities { "coordinator": false, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": true, "queryType": "none", @@ -184,6 +198,7 @@ exports[`HomeView matches snapshot (overlord) 1`] = ` Capabilities { "coordinator": false, "maxTaskSlots": undefined, + "multiStageQueryDart": false, "multiStageQueryTask": false, "overlord": true, "queryType": "none", 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 6ac11bc12c70..a89b4da57d1c 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 @@ -688,10 +688,10 @@ export class ColumnTree extends React.PureComponent diff --git a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.scss b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.scss new file mode 100644 index 000000000000..a2dac446eb5c --- /dev/null +++ b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.scss @@ -0,0 +1,121 @@ +/* + * 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 '../../../variables'; + +.current-dart-panel { + position: relative; + @include card-like; + overflow: auto; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + .title { + position: relative; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + padding: 8px 10px; + user-select: none; + + .close-button { + position: absolute; + top: 2px; + right: 2px; + } + } + + .work-entries { + position: absolute; + top: 30px; + left: 0; + right: 0; + bottom: 0; + padding: 10px; + + &:empty:after { + content: 'No current queries'; + position: absolute; + top: 45%; + left: 50%; + transform: translate(-50%, -50%); + } + + .work-entry { + display: block; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .line1 { + margin-bottom: 4px; + + .status-icon { + display: inline-block; + margin-right: 5px; + + &.running { + svg { + animation-name: spin; + animation-duration: 10s; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + } + } + + .timing { + display: inline-block; + } + } + + .line2 { + white-space: nowrap; + overflow: hidden; + } + + .identity-icon { + opacity: 0.6; + } + + .identity-identity { + margin-left: 5px; + display: inline-block; + + &.anonymous { + font-style: italic; + } + } + + .query-indicator { + display: inline-block; + margin-left: 10px; + } + } + } +} diff --git a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx new file mode 100644 index 000000000000..aae00206942c --- /dev/null +++ b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx @@ -0,0 +1,194 @@ +/* + * 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 { Button, Icon, Intent, Menu, MenuDivider, MenuItem, Popover } from '@blueprintjs/core'; +import { type IconName, IconNames } from '@blueprintjs/icons'; +import classNames from 'classnames'; +import copy from 'copy-to-clipboard'; +import React, { useCallback, useState } from 'react'; +import { useStore } from 'zustand'; + +import { Loader } from '../../../components'; +import type { DartQueryEntry } from '../../../druid-models'; +import { useClock, useInterval, useQueryManager } from '../../../hooks'; +import { Api, AppToaster } from '../../../singletons'; +import { formatDuration, prettyFormatIsoDate } from '../../../utils'; +import { CancelQueryDialog } from '../cancel-query-dialog/cancel-query-dialog'; +import { DartDetailsDialog } from '../dart-details-dialog/dart-details-dialog'; +import { workStateStore } from '../work-state-store'; + +import './current-dart-panel.scss'; + +function stateToIconAndColor(status: DartQueryEntry['state']): [IconName, string] { + switch (status) { + case 'RUNNING': + return [IconNames.REFRESH, '#2167d5']; + case 'ACCEPTED': + return [IconNames.CIRCLE, '#8d8d8d']; + case 'CANCELED': + return [IconNames.DISABLE, '#8d8d8d']; + default: + return [IconNames.CIRCLE, '#8d8d8d']; + } +} + +export interface CurrentViberPanelProps { + onClose(): void; +} + +export const CurrentDartPanel = React.memo(function CurrentViberPanel( + props: CurrentViberPanelProps, +) { + const { onClose } = props; + + const [showSql, setShowSql] = useState(); + const [confirmCancelId, setConfirmCancelId] = useState(); + + const workStateVersion = useStore( + workStateStore, + useCallback(state => state.version, []), + ); + + const [dartQueryEntriesState, queryManager] = useQueryManager({ + query: workStateVersion, + processQuery: async _ => { + return (await Api.instance.get('/druid/v2/sql/dart')).data.queries; + }, + }); + + useInterval(() => { + queryManager.rerunLastQuery(true); + }, 3000); + + const now = useClock(); + + const dartQueryEntries = dartQueryEntriesState.getSomeData(); + return ( +
+
+ Current Dart queries +
+ {dartQueryEntries ? ( +
+ {dartQueryEntries.map(w => { + const menu = ( + + { + setShowSql(w.sql); + }} + /> + { + copy(w.sqlQueryId, { format: 'text/plain' }); + AppToaster.show({ + message: `${w.sqlQueryId} copied to clipboard`, + intent: Intent.SUCCESS, + }); + }} + /> + { + copy(w.dartQueryId, { format: 'text/plain' }); + AppToaster.show({ + message: `${w.dartQueryId} copied to clipboard`, + intent: Intent.SUCCESS, + }); + }} + /> + + setConfirmCancelId(w.sqlQueryId)} + /> + + ); + + const duration = now.valueOf() - new Date(w.startTime).valueOf(); + + const [icon, color] = stateToIconAndColor(w.state); + const anonymous = w.identity === 'allowAll' && w.authenticator === 'allowAll'; + return ( + +
+
+ +
+ {prettyFormatIsoDate(w.startTime) + + ((w.state === 'RUNNING' || w.state === 'ACCEPTED') && duration > 0 + ? ` (${formatDuration(duration)})` + : '')} +
+
+
+ +
+ {anonymous ? 'anonymous' : `${w.identity} (${w.authenticator})`} +
+
+
+
+ ); + })} +
+ ) : dartQueryEntriesState.isLoading() ? ( + + ) : undefined} + {confirmCancelId && ( + { + if (!confirmCancelId) return; + try { + await Api.instance.delete(`/druid/v2/sql/dart/${Api.encodePath(confirmCancelId)}`); + + AppToaster.show({ + message: 'Query canceled', + intent: Intent.SUCCESS, + }); + } catch { + AppToaster.show({ + message: 'Could not cancel query', + intent: Intent.DANGER, + }); + } + }} + onDismiss={() => setConfirmCancelId(undefined)} + /> + )} + {showSql && setShowSql(undefined)} />} +
+ ); +}); diff --git a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss new file mode 100644 index 000000000000..f1f380dc4ec8 --- /dev/null +++ b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss @@ -0,0 +1,35 @@ +/* + * 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 '../../../variables'; + +.dart-details-dialog { + &.#{$bp-ns}-dialog { + width: 95vw; + } + + .#{$bp-ns}-dialog-body { + height: 70vh; + position: relative; + margin: 0; + + .flexible-query-input { + height: 100%; + } + } +} diff --git a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx new file mode 100644 index 000000000000..0637d6b9644b --- /dev/null +++ b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx @@ -0,0 +1,48 @@ +/* + * 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 { Button, Classes, Dialog } from '@blueprintjs/core'; +import React from 'react'; + +import { FlexibleQueryInput } from '../flexible-query-input/flexible-query-input'; + +import './dart-details-dialog.scss'; + +export interface DartDetailsDialogProps { + sql: string; + onClose(): void; +} + +export const DartDetailsDialog = React.memo(function DartDetailsDialog( + props: DartDetailsDialogProps, +) { + const { sql, onClose } = props; + + return ( + +
+ +
+
+
+
+
+
+ ); +}); diff --git a/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap b/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap index 3ab5ab102163..4f9706930e78 100644 --- a/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap +++ b/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap @@ -134,12 +134,12 @@ exports[`ExecutionStagesPane matches snapshot 1`] = ` - counter + Counter - wall time + Wall time , @@ -147,7 +147,7 @@ exports[`ExecutionStagesPane matches snapshot 1`] = ` "className": "padded", "id": "cpu", "show": false, - "width": 220, + "width": 240, }, { "Header": diff --git a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.scss b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.scss index 6a4ffce769e3..e584de3216b0 100644 --- a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.scss +++ b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.scss @@ -129,7 +129,7 @@ .cpu-label { display: inline-block; - width: 120px; + width: 140px; } .cpu-counter { diff --git a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx index 4ba0ed54aaa1..322b9a8544bd 100644 --- a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx +++ b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx @@ -263,8 +263,8 @@ export const ExecutionStagesPane = React.memo(function ExecutionStagesPane( Header: twoLines( 'CPU utilization', - counter - wall time + Counter + Wall time , ), id: 'cpu', @@ -863,14 +863,14 @@ ${title} uncompressed size: ${formatBytesCompact( Header: twoLines( 'CPU utilization', - counter - wall time + Counter + Wall time , ), id: 'cpu', accessor: () => null, className: 'padded', - width: 220, + width: 240, show: stages.hasCounter('cpu'), Cell({ original }) { const cpuTotals = stages.getCpuTotalsForStage(original); diff --git a/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx b/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx index d0f361931dc9..b1930ae50d2b 100644 --- a/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx +++ b/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx @@ -96,7 +96,7 @@ export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel( } onClick={() => { if (!execution) return; - if (oneOf(execution.engine, 'sql-msq-task')) { + if (oneOf(execution.engine, 'sql-msq-task', 'sql-msq-dart')) { onExecutionDetail(); } }} diff --git a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx index 2080bf47256b..3f9c3ea9a3d1 100644 --- a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx +++ b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx @@ -45,6 +45,7 @@ import { getDruidErrorMessage, nonEmptyArray, queryDruidSql, + queryDruidSqlDart, } from '../../../utils'; import './explain-dialog.scss'; @@ -108,6 +109,10 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia } break; + case 'sql-msq-dart': + result = await queryDruidSqlDart(payload); + break; + default: throw new Error(`Explain not supported for engine ${engine}`); } diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx b/web-console/src/views/workbench-view/query-tab/query-tab.tsx index f477a51650b6..59b4625c87e9 100644 --- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx +++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx @@ -18,7 +18,7 @@ import { Code, Intent } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { QueryRunner, SqlQuery } from '@druid-toolkit/query'; +import { QueryResult, QueryRunner, SqlQuery } from '@druid-toolkit/query'; import axios from 'axios'; import type { JSX } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; @@ -41,6 +41,7 @@ import type { WorkbenchRunningPromise } from '../../../singletons/workbench-runn import { WorkbenchRunningPromises } from '../../../singletons/workbench-running-promises'; import type { ColumnMetadata, QueryAction, QuerySlice, RowColumn } from '../../../utils'; import { + deepGet, DruidError, findAllSqlQueriesInText, localStorageGet, @@ -271,6 +272,67 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) { return execution; } + + case 'sql-msq-dart': { + if (cancelQueryId) { + void cancelToken.promise + .then(cancel => { + if (cancel.message === QueryManager.TERMINATION_MESSAGE) return; + return Api.instance.delete(`/druid/v2/sql/dart/${Api.encodePath(cancelQueryId)}`); + }) + .catch(() => {}); + } + + onQueryChange(props.query.changeLastExecution(undefined)); + + const executionPromise = Api.instance + .post(`/druid/v2/sql/dart`, query, { + cancelToken: new axios.CancelToken(cancelFn => { + nativeQueryCancelFnRef.current = cancelFn; + }), + }) + .then( + ({ data: dartResponse }) => { + if (deepGet(query, 'context.fullReport') && dartResponse[0][0] === 'fullReport') { + const dartReport = dartResponse[dartResponse.length - 1][0]; + + return Execution.fromTaskReport(dartReport) + .changeEngine('sql-msq-dart') + .changeSqlQuery(query.query, query.context); + } else { + return Execution.fromResult( + engine, + QueryResult.fromRawResult( + dartResponse, + false, + query.header, + query.typesHeader, + query.sqlTypesHeader, + ), + ).changeSqlQuery(query.query, query.context); + } + }, + e => { + throw new DruidError(e, prefixLines); + }, + ); + + WorkbenchRunningPromises.storePromise(id, { + executionPromise, + startTime, + }); + + let execution: Execution; + try { + execution = await executionPromise; + nativeQueryCancelFnRef.current = undefined; + } catch (e) { + nativeQueryCancelFnRef.current = undefined; + throw e; + } + + return execution; + } } } else if (WorkbenchRunningPromises.isWorkbenchRunningPromise(q)) { return await q.executionPromise; @@ -463,13 +525,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
)} {execution && - (execution.result ? ( - - ) : execution.error ? ( + (execution.error ? (
{execution.stages && ( @@ -481,6 +537,12 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) { /> )}
+ ) : execution.result ? ( + ) : execution.isSuccessfulIngest() ? (
{prettyFormatIsoDate(w.createdTime) + diff --git a/web-console/src/views/workbench-view/run-panel/__snapshots__/run-panel.spec.tsx.snap b/web-console/src/views/workbench-view/run-panel/__snapshots__/run-panel.spec.tsx.snap index 51e5f34da2bd..620185db9d2b 100644 --- a/web-console/src/views/workbench-view/run-panel/__snapshots__/run-panel.spec.tsx.snap +++ b/web-console/src/views/workbench-view/run-panel/__snapshots__/run-panel.spec.tsx.snap @@ -46,7 +46,7 @@ exports[`RunPanel matches snapshot on msq (auto) query 1`] = ` - Engine: SQL MSQ-task + Engine: SQL (task)
)} {this.renderExecutionDetailsDialog()}