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
diff --git a/web-console/script/druid b/web-console/script/druid
index 122feba..e7e575a 100755
--- a/web-console/script/druid
+++ b/web-console/script/druid
@@ -67,6 +67,7 @@
&& 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 c3310e2..d3e24a6 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 @@
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 0000000..f2409ab
--- /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 0000000..472248b
--- /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 f1942e5..335d22e 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 e768afe..dfeeeea 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -20,6 +20,7 @@
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 fbb2c1c..ddddeab 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 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 dd75c94..716fe57 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 @@
};
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 @@
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 fe125b6..013f936 100644
--- a/web-console/src/helpers/capabilities.ts
+++ b/web-console/src/helpers/capabilities.ts
@@ -37,6 +37,7 @@
export interface CapabilitiesValue {
queryType: QueryType;
multiStageQueryTask: boolean;
+ multiStageQueryDart: boolean;
coordinator: boolean;
overlord: boolean;
maxTaskSlots?: number;
@@ -53,6 +54,7 @@
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 @@
}
}
+ static async detectMultiStageQueryDart(): Promise<boolean> {
+ 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<Capabilities | undefined> {
const queryType = await Capabilities.detectQueryType();
if (typeof queryType === 'undefined') return;
@@ -154,11 +165,15 @@
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 @@
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 @@
return {
queryType: this.queryType,
multiStageQueryTask: this.multiStageQueryTask,
+ multiStageQueryDart: this.multiStageQueryDart,
coordinator: this.coordinator,
overlord: this.overlord,
maxTaskSlots: this.maxTaskSlots,
@@ -248,6 +265,10 @@
return this.multiStageQueryTask;
}
+ public hasMultiStageQueryDart(): boolean {
+ return this.multiStageQueryDart;
+ }
+
public getSupportedQueryEngines(): DruidEngine[] {
const queryEngines: DruidEngine[] = ['native'];
if (this.hasSql()) {
@@ -256,6 +277,9 @@
if (this.hasMultiStageQueryTask()) {
queryEngines.push('sql-msq-task');
}
+ if (this.hasMultiStageQueryDart()) {
+ queryEngines.push('sql-msq-dart');
+ }
return queryEngines;
}
@@ -282,36 +306,42 @@
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 fba63b9..d148136 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -342,6 +342,19 @@
return sqlResultResp.data;
}
+export async function queryDruidSqlDart<T = any>(
+ sqlQueryPayload: Record<string, any>,
+ cancelToken?: CancelToken,
+): Promise<T[]> {
+ 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 d4efec0..8a8fee9 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -53,6 +53,7 @@
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 9223fb7..02ac850 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 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": false,
"queryType": "none",
@@ -21,6 +22,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": false,
"queryType": "none",
@@ -32,6 +34,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": false,
"queryType": "none",
@@ -44,6 +47,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": false,
"queryType": "none",
@@ -55,6 +59,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": false,
"queryType": "none",
@@ -73,6 +78,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -85,6 +91,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -96,6 +103,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -109,6 +117,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -120,6 +129,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -132,6 +142,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -143,6 +154,7 @@
Capabilities {
"coordinator": true,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": true,
"multiStageQueryTask": true,
"overlord": true,
"queryType": "nativeAndSql",
@@ -161,6 +173,7 @@
Capabilities {
"coordinator": false,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": true,
"queryType": "none",
@@ -173,6 +186,7 @@
Capabilities {
"coordinator": false,
"maxTaskSlots": undefined,
+ "multiStageQueryDart": false,
"multiStageQueryTask": false,
"overlord": true,
"queryType": "none",
@@ -184,6 +198,7 @@
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 6ac11bc..a89b4da 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 @@
};
render() {
- const { columnMetadataLoading } = this.props;
+ const { columnMetadata, columnMetadataLoading } = this.props;
const { currentSchemaSubtree, searchString } = this.state;
- if (columnMetadataLoading) {
+ if (columnMetadataLoading && !columnMetadata) {
return (
<div className="column-tree">
<Loader />
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 0000000..a2dac44
--- /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 0000000..aae0020
--- /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<string | undefined>();
+ const [confirmCancelId, setConfirmCancelId] = useState<string | undefined>();
+
+ const workStateVersion = useStore(
+ workStateStore,
+ useCallback(state => state.version, []),
+ );
+
+ const [dartQueryEntriesState, queryManager] = useQueryManager<number, DartQueryEntry[]>({
+ 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 (
+ <div className="current-dart-panel">
+ <div className="title">
+ Current Dart queries
+ <Button className="close-button" icon={IconNames.CROSS} minimal onClick={onClose} />
+ </div>
+ {dartQueryEntries ? (
+ <div className="work-entries">
+ {dartQueryEntries.map(w => {
+ const menu = (
+ <Menu>
+ <MenuItem
+ icon={IconNames.EYE_OPEN}
+ text="Show SQL"
+ onClick={() => {
+ setShowSql(w.sql);
+ }}
+ />
+ <MenuItem
+ icon={IconNames.DUPLICATE}
+ text="Copy SQL ID"
+ onClick={() => {
+ copy(w.sqlQueryId, { format: 'text/plain' });
+ AppToaster.show({
+ message: `${w.sqlQueryId} copied to clipboard`,
+ intent: Intent.SUCCESS,
+ });
+ }}
+ />
+ <MenuItem
+ icon={IconNames.DUPLICATE}
+ text="Copy Dart ID"
+ onClick={() => {
+ copy(w.dartQueryId, { format: 'text/plain' });
+ AppToaster.show({
+ message: `${w.dartQueryId} copied to clipboard`,
+ intent: Intent.SUCCESS,
+ });
+ }}
+ />
+ <MenuDivider />
+ <MenuItem
+ icon={IconNames.CROSS}
+ text="Cancel query"
+ intent={Intent.DANGER}
+ onClick={() => setConfirmCancelId(w.sqlQueryId)}
+ />
+ </Menu>
+ );
+
+ const duration = now.valueOf() - new Date(w.startTime).valueOf();
+
+ const [icon, color] = stateToIconAndColor(w.state);
+ const anonymous = w.identity === 'allowAll' && w.authenticator === 'allowAll';
+ return (
+ <Popover className="work-entry" key={w.sqlQueryId} position="left" content={menu}>
+ <div>
+ <div className="line1">
+ <Icon
+ className={'status-icon ' + w.state.toLowerCase()}
+ icon={icon}
+ style={{ color }}
+ data-tooltip={`State: ${w.state}`}
+ />
+ <div className="timing">
+ {prettyFormatIsoDate(w.startTime) +
+ ((w.state === 'RUNNING' || w.state === 'ACCEPTED') && duration > 0
+ ? ` (${formatDuration(duration)})`
+ : '')}
+ </div>
+ </div>
+ <div className="line2">
+ <Icon className="identity-icon" icon={IconNames.MUGSHOT} />
+ <div
+ className={classNames('identity-identity', { anonymous })}
+ data-tooltip={`Identity: ${w.identity}\nAuthenticator: ${w.authenticator}`}
+ >
+ {anonymous ? 'anonymous' : `${w.identity} (${w.authenticator})`}
+ </div>
+ </div>
+ </div>
+ </Popover>
+ );
+ })}
+ </div>
+ ) : dartQueryEntriesState.isLoading() ? (
+ <Loader />
+ ) : undefined}
+ {confirmCancelId && (
+ <CancelQueryDialog
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ onCancel={async () => {
+ 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 && <DartDetailsDialog sql={showSql} onClose={() => setShowSql(undefined)} />}
+ </div>
+ );
+});
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 0000000..f1f380d
--- /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 0000000..0637d6b
--- /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 (
+ <Dialog className="dart-details-dialog" isOpen onClose={onClose} title="Dart SQL">
+ <div className={Classes.DIALOG_BODY}>
+ <FlexibleQueryInput queryString={sql} leaveBackground />
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button text="Close" onClick={onClose} />
+ </div>
+ </div>
+ </Dialog>
+ );
+});
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 3ab5ab1..4f97069 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 @@
<span
className="cpu-label"
>
- counter
+ Counter
</span>
<span
className="cpu-counter"
>
- wall time
+ Wall time
</span>
</i>
</React.Fragment>,
@@ -147,7 +147,7 @@
"className": "padded",
"id": "cpu",
"show": false,
- "width": 220,
+ "width": 240,
},
{
"Header": <React.Fragment>
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 6a4ffce..e584de3 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 4ba0ed5..322b9a8 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 @@
Header: twoLines(
'CPU utilization',
<i>
- <span className="cpu-label">counter</span>
- <span className="cpu-counter">wall time</span>
+ <span className="cpu-label">Counter</span>
+ <span className="cpu-counter">Wall time</span>
</i>,
),
id: 'cpu',
@@ -863,14 +863,14 @@
Header: twoLines(
'CPU utilization',
<i>
- <span className="cpu-label">counter</span>
- <span className="cpu-counter">wall time</span>
+ <span className="cpu-label">Counter</span>
+ <span className="cpu-counter">Wall time</span>
</i>,
),
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 d0f3619..b1930ae 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 @@
}
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 2080bf4..3f9c3ea 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 @@
getDruidErrorMessage,
nonEmptyArray,
queryDruidSql,
+ queryDruidSqlDart,
} from '../../../utils';
import './explain-dialog.scss';
@@ -108,6 +109,10 @@
}
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 f477a51..59b4625 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 { WorkbenchRunningPromises } from '../../../singletons/workbench-running-promises';
import type { ColumnMetadata, QueryAction, QuerySlice, RowColumn } from '../../../utils';
import {
+ deepGet,
DruidError,
findAllSqlQueriesInText,
localStorageGet,
@@ -271,6 +272,67 @@
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 @@
</div>
)}
{execution &&
- (execution.result ? (
- <ResultTablePane
- runeMode={execution.engine === 'native'}
- queryResult={execution.result}
- onQueryAction={handleQueryAction}
- />
- ) : execution.error ? (
+ (execution.error ? (
<div className="error-container">
<ExecutionErrorPane execution={execution} />
{execution.stages && (
@@ -481,6 +537,12 @@
/>
)}
</div>
+ ) : execution.result ? (
+ <ResultTablePane
+ runeMode={execution.engine === 'native'}
+ queryResult={execution.result}
+ onQueryAction={handleQueryAction}
+ />
) : execution.isSuccessfulIngest() ? (
<IngestSuccessPane
execution={execution}
diff --git a/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx b/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
index 1380335..1a08fff 100644
--- a/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
+++ b/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
@@ -225,6 +225,7 @@
className={'status-icon ' + w.taskStatus.toLowerCase()}
icon={icon}
style={{ color }}
+ data-tooltip={`Task status: ${w.taskStatus}`}
/>
<div className="timing">
{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 51e5f34..620185d 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 @@
<span
class="bp5-button-text"
>
- Engine: SQL MSQ-task
+ Engine: SQL (task)
</span>
<span
aria-hidden="true"
@@ -150,7 +150,7 @@
<span
class="bp5-button-text"
>
- Engine: Auto (SQL native)
+ Engine: Auto [SQL (native)]
</span>
<span
aria-hidden="true"
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index ddec827..d586959 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -103,13 +103,16 @@
if (!engine) return { text: 'Auto' };
switch (engine) {
case 'native':
- return { text: 'Native' };
+ return { text: 'JSON (native)' };
case 'sql-native':
- return { text: 'SQL native' };
+ return { text: 'SQL (native)' };
case 'sql-msq-task':
- return { text: 'SQL MSQ-task', label: 'multi-stage-query' };
+ return { text: 'SQL (task)', label: 'multi-stage-query' };
+
+ case 'sql-msq-dart':
+ return { text: 'SQL (Dart)', label: 'multi-stage-query' };
default:
return { text: engine };
@@ -121,8 +124,6 @@
durableStorage: 'Durable storage',
};
-const EXPERIMENTAL_ICON = <Icon icon={IconNames.WARNING_SIGN} title="Experimental" />;
-
export type EnginesMenuOption =
| 'edit-query-context'
| 'define-parameters'
@@ -135,7 +136,6 @@
| 'finalize-aggregations'
| 'group-by-enable-multi-value-unnesting'
| 'durable-shuffle-storage'
- | 'include-all-counters'
| 'use-cache'
| 'approximate-top-n'
| 'limit-inline-results';
@@ -158,21 +158,24 @@
case 'finalize-aggregations':
case 'group-by-enable-multi-value-unnesting':
case 'durable-shuffle-storage':
- case 'include-all-counters':
- case 'join-algorithm':
return engine === 'sql-msq-task';
+ case 'join-algorithm':
+ return engine === 'sql-msq-task' || engine === 'sql-msq-dart';
+
case 'timezone':
case 'approximate-count-distinct':
- return engine === 'sql-native' || engine === 'sql-msq-task';
+ return engine === 'sql-native' || engine === 'sql-msq-task' || engine === 'sql-msq-dart';
case 'use-cache':
return engine === 'native' || engine === 'sql-native';
case 'approximate-top-n':
- case 'limit-inline-results':
return engine === 'sql-native';
+ case 'limit-inline-results':
+ return engine === 'sql-native' || engine === 'sql-msq-dart';
+
default:
console.warn(`Unknown option: ${option}`);
return false;
@@ -251,16 +254,6 @@
queryContext,
defaultQueryContext,
);
- const useConcurrentLocks = getQueryContextKey(
- 'useConcurrentLocks',
- queryContext,
- defaultQueryContext,
- );
- const forceSegmentSortByTime = getQueryContextKey(
- 'forceSegmentSortByTime',
- queryContext,
- defaultQueryContext,
- );
const finalizeAggregations = queryContext.finalizeAggregations;
const waitUntilSegmentsLoad = queryContext.waitUntilSegmentsLoad;
const groupByEnableMultiValueUnnesting = queryContext.groupByEnableMultiValueUnnesting;
@@ -279,11 +272,6 @@
queryContext,
defaultQueryContext,
);
- const includeAllCounters = getQueryContextKey(
- 'includeAllCounters',
- queryContext,
- defaultQueryContext,
- );
const indexSpec: IndexSpec | undefined = deepGet(queryContext, 'indexSpec');
@@ -385,7 +373,7 @@
<Menu>
{queryEngines.length > 1 && (
<>
- <MenuDivider title="Select engine" />
+ <MenuDivider title="Select language and engine" />
<MenuItem
key="auto"
icon={tickIcon(queryEngine === undefined)}
@@ -469,81 +457,33 @@
/>
</MenuItem>
)}
-
- {show('insert-replace-specific-context') && (
- <MenuItem icon={IconNames.BRING_DATA} text="INSERT / REPLACE specific context">
- <MenuBoolean
- text="Force segment sort by time"
- value={forceSegmentSortByTime}
- onValueChange={forceSegmentSortByTime =>
- changeQueryContext({
- ...queryContext,
- forceSegmentSortByTime,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- optionsLabelElement={{ false: EXPERIMENTAL_ICON }}
- />
- <MenuBoolean
- text="Use concurrent locks"
- value={useConcurrentLocks}
- onValueChange={useConcurrentLocks =>
- changeQueryContext({
- ...queryContext,
- useConcurrentLocks,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- optionsLabelElement={{ true: EXPERIMENTAL_ICON }}
- />
- <MenuBoolean
- text="Fail on empty insert"
- value={failOnEmptyInsert}
- showUndefined
- undefinedEffectiveValue={false}
- onValueChange={failOnEmptyInsert =>
- changeQueryContext({ ...queryContext, failOnEmptyInsert })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- <MenuBoolean
- text="Wait until segments have loaded"
- value={waitUntilSegmentsLoad}
- showUndefined
- undefinedEffectiveValue={ingestMode}
- onValueChange={waitUntilSegmentsLoad =>
- changeQueryContext({ ...queryContext, waitUntilSegmentsLoad })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- <MenuItem
- text="Edit index spec..."
- label={summarizeIndexSpec(indexSpec)}
- shouldDismissPopover={false}
- onClick={() => {
- setIndexSpecDialogSpec(indexSpec || {});
- }}
- />
- </MenuItem>
+ {show('approximate-count-distinct') && (
+ <MenuBoolean
+ icon={IconNames.ROCKET_SLANT}
+ text="Approximate COUNT(DISTINCT)"
+ value={useApproximateCountDistinct}
+ onValueChange={useApproximateCountDistinct =>
+ changeQueryContext({
+ ...queryContext,
+ useApproximateCountDistinct,
+ })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
)}
- {show('max-parse-exceptions') && (
- <MenuItem
- icon={IconNames.ERROR}
- text="Max parse exceptions"
- label={String(maxParseExceptions)}
- >
- {[0, 1, 5, 10, 1000, 10000, -1].map(v => (
- <MenuItem
- key={String(v)}
- icon={tickIcon(v === maxParseExceptions)}
- text={v === -1 ? '∞ (-1)' : String(v)}
- onClick={() =>
- changeQueryContext({ ...queryContext, maxParseExceptions: v })
- }
- shouldDismissPopover={false}
- />
- ))}
- </MenuItem>
+ {show('approximate-top-n') && (
+ <MenuBoolean
+ icon={IconNames.HORIZONTAL_BAR_CHART_DESC}
+ text="Approximate TopN"
+ value={useApproximateTopN}
+ onValueChange={useApproximateTopN =>
+ changeQueryContext({
+ ...queryContext,
+ useApproximateTopN,
+ })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
)}
{show('join-algorithm') && (
<MenuItem
@@ -566,6 +506,125 @@
</MenuItem>
)}
+ {show('insert-replace-specific-context') && (
+ <MenuItem
+ icon={IconNames.BRING_DATA}
+ text="INSERT / REPLACE / EXTERN specific context"
+ >
+ <MenuBoolean
+ text="Fail on empty insert"
+ value={failOnEmptyInsert}
+ showUndefined
+ undefinedEffectiveValue={false}
+ onValueChange={failOnEmptyInsert =>
+ changeQueryContext({ ...queryContext, failOnEmptyInsert })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ <MenuBoolean
+ text="Wait until segments have loaded"
+ value={waitUntilSegmentsLoad}
+ showUndefined
+ undefinedEffectiveValue={ingestMode}
+ onValueChange={waitUntilSegmentsLoad =>
+ changeQueryContext({ ...queryContext, waitUntilSegmentsLoad })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ <MenuItem text="Max parse exceptions" label={String(maxParseExceptions)}>
+ {[0, 1, 5, 10, 1000, 10000, -1].map(v => (
+ <MenuItem
+ key={String(v)}
+ icon={tickIcon(v === maxParseExceptions)}
+ text={v === -1 ? '∞ (-1)' : String(v)}
+ onClick={() =>
+ changeQueryContext({ ...queryContext, maxParseExceptions: v })
+ }
+ shouldDismissPopover={false}
+ />
+ ))}
+ </MenuItem>
+ <MenuItem
+ text="Edit index spec..."
+ label={summarizeIndexSpec(indexSpec)}
+ shouldDismissPopover={false}
+ onClick={() => {
+ setIndexSpecDialogSpec(indexSpec || {});
+ }}
+ />
+ </MenuItem>
+ )}
+
+ {show('finalize-aggregations') && (
+ <MenuBoolean
+ icon={IconNames.TRANSLATE}
+ text="Finalize aggregations"
+ value={finalizeAggregations}
+ showUndefined
+ undefinedEffectiveValue={!ingestMode}
+ onValueChange={finalizeAggregations =>
+ changeQueryContext({ ...queryContext, finalizeAggregations })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ )}
+ {show('group-by-enable-multi-value-unnesting') && (
+ <MenuBoolean
+ icon={IconNames.FORK}
+ text="GROUP BY multi-value unnesting"
+ value={groupByEnableMultiValueUnnesting}
+ showUndefined
+ undefinedEffectiveValue={!ingestMode}
+ onValueChange={groupByEnableMultiValueUnnesting =>
+ changeQueryContext({ ...queryContext, groupByEnableMultiValueUnnesting })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ )}
+
+ {show('use-cache') && (
+ <MenuBoolean
+ icon={IconNames.DATA_CONNECTION}
+ text="Use cache"
+ value={useCache}
+ onValueChange={useCache =>
+ changeQueryContext({
+ ...queryContext,
+ useCache,
+ populateCache: useCache,
+ })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ )}
+ {show('limit-inline-results') && (
+ <MenuCheckbox
+ checked={!query.unlimited}
+ intent={query.unlimited ? Intent.WARNING : undefined}
+ text="Limit inline results"
+ labelElement={
+ query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
+ }
+ onChange={() => {
+ onQueryChange(query.toggleUnlimited());
+ }}
+ />
+ )}
+
+ {show('durable-shuffle-storage') && (
+ <MenuBoolean
+ icon={IconNames.CLOUD_TICK}
+ text="Durable shuffle storage"
+ value={durableShuffleStorage}
+ onValueChange={durableShuffleStorage =>
+ changeQueryContext({
+ ...queryContext,
+ durableShuffleStorage,
+ })
+ }
+ optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
+ />
+ )}
{show('select-destination') && (
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
@@ -602,119 +661,6 @@
/>
</MenuItem>
)}
-
- {show('finalize-aggregations') && (
- <MenuBoolean
- icon={IconNames.TRANSLATE}
- text="Finalize aggregations"
- value={finalizeAggregations}
- showUndefined
- undefinedEffectiveValue={!ingestMode}
- onValueChange={finalizeAggregations =>
- changeQueryContext({ ...queryContext, finalizeAggregations })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
- {show('group-by-enable-multi-value-unnesting') && (
- <MenuBoolean
- icon={IconNames.FORK}
- text="GROUP BY multi-value unnesting"
- value={groupByEnableMultiValueUnnesting}
- showUndefined
- undefinedEffectiveValue={!ingestMode}
- onValueChange={groupByEnableMultiValueUnnesting =>
- changeQueryContext({ ...queryContext, groupByEnableMultiValueUnnesting })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
- {show('durable-shuffle-storage') && (
- <MenuBoolean
- icon={IconNames.CLOUD_TICK}
- text="Durable shuffle storage"
- value={durableShuffleStorage}
- onValueChange={durableShuffleStorage =>
- changeQueryContext({
- ...queryContext,
- durableShuffleStorage,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
-
- {show('use-cache') && (
- <MenuBoolean
- icon={IconNames.DATA_CONNECTION}
- text="Use cache"
- value={useCache}
- onValueChange={useCache =>
- changeQueryContext({
- ...queryContext,
- useCache,
- populateCache: useCache,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
- {show('approximate-top-n') && (
- <MenuBoolean
- icon={IconNames.HORIZONTAL_BAR_CHART_DESC}
- text="Approximate TopN"
- value={useApproximateTopN}
- onValueChange={useApproximateTopN =>
- changeQueryContext({
- ...queryContext,
- useApproximateTopN,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
-
- {show('approximate-count-distinct') && (
- <MenuBoolean
- icon={IconNames.ROCKET_SLANT}
- text="Approximate COUNT(DISTINCT)"
- value={useApproximateCountDistinct}
- onValueChange={useApproximateCountDistinct =>
- changeQueryContext({
- ...queryContext,
- useApproximateCountDistinct,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
- {show('limit-inline-results') && (
- <MenuCheckbox
- checked={!query.unlimited}
- intent={query.unlimited ? Intent.WARNING : undefined}
- text="Limit inline results"
- labelElement={
- query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
- }
- onChange={() => {
- onQueryChange(query.toggleUnlimited());
- }}
- />
- )}
- {show('include-all-counters') && (
- <MenuBoolean
- icon={IconNames.DIAGNOSIS}
- text="Include all counters"
- value={includeAllCounters}
- onValueChange={includeAllCounters =>
- changeQueryContext({
- ...queryContext,
- includeAllCounters,
- })
- }
- optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
- />
- )}
</Menu>
}
>
@@ -722,7 +668,7 @@
text={`Engine: ${
queryEngine
? enginesLabelFn(queryEngine).text
- : `${autoEngineLabel.text} (${enginesLabelFn(effectiveEngine).text})`
+ : `${autoEngineLabel.text} [${enginesLabelFn(effectiveEngine).text}]`
}`}
rightIcon={IconNames.CARET_DOWN}
intent={intent}
diff --git a/web-console/src/views/workbench-view/workbench-view.scss b/web-console/src/views/workbench-view/workbench-view.scss
index 1287dab..be00623 100644
--- a/web-console/src/views/workbench-view/workbench-view.scss
+++ b/web-console/src/views/workbench-view/workbench-view.scss
@@ -45,7 +45,7 @@
gap: 2px;
.recent-query-task-panel,
- .current-viper-panel {
+ .current-dart-panel {
flex: 1;
}
}
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx b/web-console/src/views/workbench-view/workbench-view.tsx
index cd4afb8..5250373 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -32,6 +32,7 @@
import copy from 'copy-to-clipboard';
import React from 'react';
+import { MenuCheckbox } from '../../components';
import { SpecDialog, StringInputDialog } from '../../dialogs';
import type {
CapacityInfo,
@@ -69,6 +70,7 @@
import { ColumnTree } from './column-tree/column-tree';
import { ConnectExternalDataDialog } from './connect-external-data-dialog/connect-external-data-dialog';
+import { CurrentDartPanel } from './current-dart-panel/current-dart-panel';
import { getDemoQueries } from './demo-queries';
import { ExecutionDetailsDialog } from './execution-details-dialog/execution-details-dialog';
import type { ExecutionDetailsTab } from './execution-details-pane/execution-details-pane';
@@ -148,6 +150,7 @@
renamingTab?: TabEntry;
showRecentQueryTaskPanel: boolean;
+ showCurrentDartPanel: boolean;
}
export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, WorkbenchViewState> {
@@ -166,6 +169,11 @@
hasSqlTask && localStorageGetJson(LocalStorageKeys.WORKBENCH_TASK_PANEL),
);
+ const showCurrentDartPanel = Boolean(
+ queryEngines.includes('sql-msq-dart') &&
+ localStorageGetJson(LocalStorageKeys.WORKBENCH_DART_PANEL),
+ );
+
const tabEntries =
Array.isArray(possibleTabEntries) && possibleTabEntries.length
? possibleTabEntries.map(q => ({ ...q, query: new WorkbenchQuery(q.query) }))
@@ -198,6 +206,7 @@
taskIdSubmitDialogOpen: false,
showRecentQueryTaskPanel,
+ showCurrentDartPanel,
};
}
@@ -264,6 +273,11 @@
localStorageSetJson(LocalStorageKeys.WORKBENCH_TASK_PANEL, false);
};
+ private readonly handleCurrentDartPanelClose = () => {
+ this.setState({ showCurrentDartPanel: false });
+ localStorageSetJson(LocalStorageKeys.WORKBENCH_DART_PANEL, false);
+ };
+
private readonly handleDetailsWithId = (id: string, initTab?: ExecutionDetailsTab) => {
this.setState({
details: { id, initTab },
@@ -656,7 +670,7 @@
if (!queryEngines.includes('sql-msq-task')) return;
if (hideToolbar) return;
- const { showRecentQueryTaskPanel } = this.state;
+ const { showRecentQueryTaskPanel, showCurrentDartPanel } = this.state;
return (
<ButtonGroup className="toolbar">
<Button
@@ -669,16 +683,35 @@
}}
minimal
/>
- <Button
- icon={IconNames.DRAWER_RIGHT}
- minimal
- data-tooltip="Open recent query task panel"
- onClick={() => {
- const n = !showRecentQueryTaskPanel;
- this.setState({ showRecentQueryTaskPanel: n });
- localStorageSetJson(LocalStorageKeys.WORKBENCH_TASK_PANEL, n);
- }}
- />
+ <Popover
+ position="bottom-right"
+ content={
+ <Menu>
+ <MenuCheckbox
+ text="Recent query task panel"
+ checked={showRecentQueryTaskPanel}
+ shouldDismissPopover
+ onChange={() => {
+ const n = !showRecentQueryTaskPanel;
+ this.setState({ showRecentQueryTaskPanel: n });
+ localStorageSetJson(LocalStorageKeys.WORKBENCH_TASK_PANEL, n);
+ }}
+ />
+ <MenuCheckbox
+ text="Current Dart query panel"
+ checked={showCurrentDartPanel}
+ shouldDismissPopover
+ onChange={() => {
+ const n = !showCurrentDartPanel;
+ this.setState({ showCurrentDartPanel: n });
+ localStorageSetJson(LocalStorageKeys.WORKBENCH_DART_PANEL, n);
+ }}
+ />
+ </Menu>
+ }
+ >
+ <Button icon={IconNames.DRAWER_RIGHT} minimal data-tooltip="Open helper panels" />
+ </Popover>
</ButtonGroup>
);
}
@@ -744,7 +777,9 @@
runMoreMenu={
<Menu>
{!hiddenMoreMenuItems.includes('explain') &&
- (effectiveEngine === 'sql-native' || effectiveEngine === 'sql-msq-task') && (
+ (effectiveEngine === 'sql-native' ||
+ effectiveEngine === 'sql-msq-task' ||
+ effectiveEngine === 'sql-msq-dart') && (
<MenuItem
icon={IconNames.CLEAN}
text="Explain SQL query"
@@ -861,7 +896,7 @@
};
render() {
- const { columnMetadataState, showRecentQueryTaskPanel } = this.state;
+ const { columnMetadataState, showRecentQueryTaskPanel, showCurrentDartPanel } = this.state;
const query = this.getCurrentQuery();
let defaultSchema: string | undefined;
@@ -872,7 +907,7 @@
defaultTables = parsedQuery.getUsedTableNames();
}
- const showRightPanel = showRecentQueryTaskPanel;
+ const showRightPanel = showRecentQueryTaskPanel || showCurrentDartPanel;
return (
<div
className={classNames('workbench-view app-view', {
@@ -883,8 +918,8 @@
{!columnMetadataState.isError() && (
<ColumnTree
getParsedQuery={this.getParsedQuery}
+ columnMetadata={columnMetadataState.getSomeData()}
columnMetadataLoading={columnMetadataState.loading}
- columnMetadata={columnMetadataState.data}
onQueryChange={this.handleSqlQueryChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTables={defaultTables}
@@ -903,6 +938,9 @@
onNewTab={this.handleNewTab}
/>
)}
+ {showCurrentDartPanel && (
+ <CurrentDartPanel onClose={this.handleCurrentDartPanelClose} />
+ )}
</div>
)}
{this.renderExecutionDetailsDialog()}