From c8529294eb914d52aa5ea0f71de8bbe342b4b920 Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Tue, 1 Oct 2024 17:53:36 -0700 Subject: [PATCH] Web console: add support for Dart engine (#17147) * add console support for Dart engine This reverts commit 6e46edf15dd55e5c51a1a4068e83deba4f22529b. * feedback fixes * surface new fields * prioratize error over results * better metadata refresh * feedback fixes --- web-console/script/druid | 1 + .../__snapshots__/header-bar.spec.tsx.snap | 1 + .../dart/dart-query-entry.mock.ts | 49 +++ .../src/druid-models/dart/dart-query-entry.ts | 27 ++ .../druid-models/druid-engine/druid-engine.ts | 9 +- web-console/src/druid-models/index.ts | 1 + web-console/src/druid-models/stages/stages.ts | 23 +- .../workbench-query/workbench-query.ts | 6 +- web-console/src/helpers/capabilities.ts | 32 +- web-console/src/utils/druid-query.ts | 13 + web-console/src/utils/local-storage-keys.tsx | 1 + .../__snapshots__/home-view.spec.tsx.snap | 15 + .../column-tree/column-tree.tsx | 4 +- .../current-dart-panel.scss | 121 +++++++ .../current-dart-panel/current-dart-panel.tsx | 194 +++++++++++ .../dart-details-dialog.scss | 35 ++ .../dart-details-dialog.tsx | 48 +++ .../execution-stages-pane.spec.tsx.snap | 6 +- .../execution-stages-pane.scss | 2 +- .../execution-stages-pane.tsx | 10 +- .../execution-summary-panel.tsx | 2 +- .../explain-dialog/explain-dialog.tsx | 5 + .../workbench-view/query-tab/query-tab.tsx | 78 ++++- .../recent-query-task-panel.tsx | 1 + .../__snapshots__/run-panel.spec.tsx.snap | 4 +- .../workbench-view/run-panel/run-panel.tsx | 300 +++++++----------- .../views/workbench-view/workbench-view.scss | 2 +- .../views/workbench-view/workbench-view.tsx | 68 +++- 28 files changed, 823 insertions(+), 235 deletions(-) create mode 100644 web-console/src/druid-models/dart/dart-query-entry.mock.ts create mode 100644 web-console/src/druid-models/dart/dart-query-entry.ts create mode 100644 web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.scss create mode 100644 web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx create mode 100644 web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss create mode 100644 web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx 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 c3310e2c590e..d3e24a6c35ac 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..8a8fee96b20d 100644 --- a/web-console/src/utils/local-storage-keys.tsx +++ b/web-console/src/utils/local-storage-keys.tsx @@ -53,6 +53,7 @@ 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, 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()}