This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 6a2c29d8091 Web console: load Dart reports (#18897)
6a2c29d8091 is described below
commit 6a2c29d80917262ed95fa414d0e4af1727467d9d
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Jan 9 18:19:57 2026 +0000
Web console: load Dart reports (#18897)
* Add retention to build
* Add inflight state checking
* set if not set in effective context
* add getEffectiveEngine checks
* getEffectiveContext
* update dart panel
* Show Dart details
* update API
* better error message for ID reuse
* fix race condition
* reformat
---
web-console/script/druid | 2 +
.../query-context-completions.ts | 1 +
.../execution/execution-ingest-complete.mock.ts | 1 +
.../execution/execution-ingest-error.mock.ts | 1 +
.../src/druid-models/execution/execution.ts | 18 +-
web-console/src/druid-models/task/task.ts | 1 +
.../workbench-query/workbench-query.spec.ts | 818 +++++++++++++++++++++
.../workbench-query/workbench-query.ts | 75 +-
.../src/utils/query-manager/query-manager.ts | 63 +-
.../views/datasources-view/datasources-view.tsx | 2 +-
.../src/views/segments-view/segments-view.tsx | 2 +-
.../views/supervisors-view/supervisors-view.tsx | 2 +-
web-console/src/views/tasks-view/tasks-view.tsx | 1 +
.../current-dart-panel/current-dart-panel.tsx | 25 +-
.../dart-details-dialog/dart-details-dialog.scss | 35 -
.../dart-details-dialog/dart-details-dialog.tsx | 48 --
.../execution-details-dialog.tsx | 4 +-
.../execution-details-pane-loader.tsx | 23 +-
.../execution-details-pane.spec.tsx.snap | 2 +-
.../execution-details-pane.tsx | 5 +-
.../execution-progress-bar-pane.tsx | 2 +-
.../views/workbench-view/query-tab/query-tab.tsx | 11 +-
.../src/views/workbench-view/workbench-view.tsx | 33 +-
23 files changed, 1044 insertions(+), 131 deletions(-)
diff --git a/web-console/script/druid b/web-console/script/druid
index 2edb40439be..c1f4f4bbcab 100755
--- a/web-console/script/druid
+++ b/web-console/script/druid
@@ -70,6 +70,8 @@ function _build_distribution() {
&& 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 \
+ && echo -e "\n\ndruid.msq.dart.controller.maxRetainedReportCount=100" >>
conf/druid/auto/_common/common.runtime.properties \
+ && echo -e
"\n\ndruid.msq.dart.controller.maxRetainedReportDuration=PT3600S" >>
conf/druid/auto/_common/common.runtime.properties \
)
}
diff --git
a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
index e74513c7e28..9e3cb8dca8d 100644
--- a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
+++ b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
@@ -110,6 +110,7 @@ export const QUERY_CONTEXT_COMPLETIONS:
JsonCompletionRule[] = [
{ value: 'forceSegmentSortByTime', documentation: 'Force segments to be
sorted by time' },
{ value: 'includeAllCounters', documentation: 'Include all counters in
task reports' },
// SQL specific
+ { value: 'sqlQueryId', documentation: 'Query ID for SQL queries' },
{ value: 'sqlTimeZone', documentation: 'Time zone for SQL queries' },
{ value: 'useApproximateCountDistinct', documentation: 'Use approximate
COUNT DISTINCT' },
{ value: 'useApproximateTopN', documentation: 'Use approximate TOP N
queries' },
diff --git
a/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
index 24ba4687ac3..cf52f2d3c4d 100644
--- a/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
+++ b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
@@ -55,6 +55,7 @@ export const EXECUTION_INGEST_COMPLETE =
Execution.fromTaskReport({
workerId: 'query-346b9ac6-4912-46e4-9b98-75f11071af87-worker0_0',
state: 'SUCCESS',
durationMs: 8789,
+ pendingMs: 123,
},
],
},
diff --git
a/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
index 0c2c5a93ed0..b4800211341 100644
--- a/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
+++ b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
@@ -92,6 +92,7 @@ export const EXECUTION_INGEST_ERROR =
Execution.fromTaskReport({
workerId: 'query-26d490c6-c06d-4cd2-938f-bc5f7f982754-worker0_0',
state: 'FAILED',
durationMs: -1,
+ pendingMs: -1,
},
],
},
diff --git a/web-console/src/druid-models/execution/execution.ts
b/web-console/src/druid-models/execution/execution.ts
index 1237b49e62d..0c327b1d064 100644
--- a/web-console/src/druid-models/execution/execution.ts
+++ b/web-console/src/druid-models/execution/execution.ts
@@ -366,8 +366,18 @@ export class Execution {
static getProgressDescription(execution: Execution | undefined): string {
if (!execution?.stages) return 'Loading...';
- if (!execution.isWaitingForQuery())
- return 'Query complete, waiting for segments to be loaded...';
+ if (!execution.isWaitingForQuery()) {
+ switch (execution.engine) {
+ case 'sql-msq-task':
+ return 'Query complete, waiting for segments to be loaded...';
+
+ case 'sql-msq-dart':
+ return 'Got a non-running report. Did you reuse a sqlQueryID?';
+
+ default:
+ return 'Query not running.';
+ }
+ }
let ret = execution.stages.getStage(0)?.phase ? 'Running query...' :
'Starting query...';
if (execution.usageInfo) {
@@ -556,6 +566,10 @@ export class Execution {
return status !== 'SUCCESS' && status !== 'FAILED';
}
+ public isWaitingForSegments(): boolean {
+ return Boolean(this.stages && !this.isWaitingForQuery() && this.engine ===
'sql-msq-task');
+ }
+
public getSegmentStatusDescription() {
const { segmentStatus } = this;
diff --git a/web-console/src/druid-models/task/task.ts
b/web-console/src/druid-models/task/task.ts
index e1743c11689..0d6cd07266f 100644
--- a/web-console/src/druid-models/task/task.ts
+++ b/web-console/src/druid-models/task/task.ts
@@ -81,6 +81,7 @@ export interface WorkerState {
workerId: string;
state: string;
durationMs: number;
+ pendingMs: number;
}
export interface SegmentLoadWaiterStatus {
diff --git
a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
index 82bbb28f69a..13908bbf2cf 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
@@ -843,4 +843,822 @@ describe('WorkbenchQuery', () => {
});
});
});
+
+ describe('#getEffectiveEngine', () => {
+ beforeEach(() => {
+ // Reset to default engines before each test
+ WorkbenchQuery.setQueryEngines(['native', 'sql-native', 'sql-msq-task']);
+ });
+
+ describe('when engine is explicitly set', () => {
+ it('returns the explicitly set engine', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeEngine('sql-msq-task');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('returns explicit engine even if query suggests different engine', ()
=> {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('INSERT INTO wiki SELECT * FROM wikipedia')
+ .changeEngine('sql-native');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns explicit engine for JSON queries', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('{"queryType": "topN", "dataSource": "test"}')
+ .changeEngine('sql-native');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns explicit engine even when context has engine set', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ engine: 'native' })
+ .changeEngine('sql-msq-task');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+ });
+
+ describe('when context engine is set', () => {
+ it('returns sql-native when context engine is native', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ engine: 'native' });
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns sql-msq-dart when context engine is msq-dart', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ engine: 'msq-dart' });
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart');
+ });
+
+ it('returns sql-native when context engine is native via SET statement',
() => {
+ const queryWithSet = sane`
+ SET engine = 'native';
+ SELECT * FROM wikipedia
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(queryWithSet);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns sql-msq-dart when context engine is msq-dart via SET
statement', () => {
+ const queryWithSet = sane`
+ SET engine = 'msq-dart';
+ SELECT * FROM wikipedia
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(queryWithSet);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart');
+ });
+
+ it('returns sql-native when context engine is native via JSON context',
() => {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {
+ "engine": "native"
+ }
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(sqlInJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns sql-msq-dart when context engine is msq-dart via JSON
context', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {
+ "engine": "msq-dart"
+ }
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(sqlInJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart');
+ });
+
+ it('prioritizes SET statement engine over JSON context engine', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SET engine = 'msq-dart'; SELECT * FROM wikipedia",
+ "context": {
+ "engine": "native"
+ }
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(sqlInJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart');
+ });
+
+ it('falls through to other logic when context engine is not native or
msq-dart', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ engine: 'msq-task' });
+
+ // Should fall through to normal logic and return sql-native
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('falls through to other logic when context engine is not set', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ // Should fall through to normal logic and return sql-native
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('handles INSERT query with context engine native', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('INSERT INTO wiki SELECT * FROM wikipedia')
+ .changeQueryContext({ engine: 'native' });
+
+ // Context engine takes priority over task engine detection
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('handles JSON query with context engine msq-dart', () => {
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia",
+ "context": {
+ "engine": "msq-dart"
+ }
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(nativeJson);
+
+ // Context engine takes priority over JSON-like detection
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart');
+ });
+ });
+
+ describe('when query is JSON-like', () => {
+ it('returns sql-native for SQL-in-JSON when sql-native is enabled', ()
=> {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {}
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(sqlInJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns native for native JSON query when native is enabled', () => {
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia",
+ "dimension": "page",
+ "threshold": 10,
+ "intervals": ["2015-09-12/2015-09-13"],
+ "granularity": "all",
+ "aggregations": [
+ {"type": "count", "name": "count"}
+ ]
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(nativeJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('native');
+ });
+
+ it('falls through for SQL-in-JSON when sql-native is not enabled', () =>
{
+ WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task']);
+
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {}
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(sqlInJson);
+
+ // Falls through JSON-like check, task engine check (no
INSERT/EXTERN), sql-native check (not enabled),
+ // and returns first enabled engine which is 'native'
+ expect(workbenchQuery.getEffectiveEngine()).toBe('native');
+ });
+
+ it('falls through for native JSON when native is not enabled', () => {
+ WorkbenchQuery.setQueryEngines(['sql-native', 'sql-msq-task']);
+
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia"
+ }
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(nativeJson);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+ });
+
+ describe('when query needs task engine', () => {
+ it('returns sql-msq-task for INSERT query when sql-msq-task is enabled',
() => {
+ const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(insertQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('returns sql-msq-task for REPLACE query when sql-msq-task is
enabled', () => {
+ const replaceQuery = 'REPLACE INTO wiki OVERWRITE ALL SELECT * FROM
wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(replaceQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('returns sql-msq-task for EXTERN query when sql-msq-task is enabled',
() => {
+ const externQuery = sane`
+ SELECT *
+ FROM TABLE(
+ EXTERN(
+ '{"type":"http","uris":["https://example.com/data.json"]}',
+ '{"type":"json"}'
+ )
+ )
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(externQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('falls through when sql-msq-task is not enabled for task engine
query', () => {
+ WorkbenchQuery.setQueryEngines(['native', 'sql-native']);
+
+ const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(insertQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+ });
+
+ describe('fallback behavior', () => {
+ it('falls back to sql-native for regular SQL query', () => {
+ const regularQuery = 'SELECT * FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(regularQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('falls back to sql-native when it is in enabled engines', () => {
+ WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task',
'sql-native']);
+
+ const regularQuery = "SELECT * FROM wikipedia WHERE channel = 'en'";
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(regularQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('falls back to first enabled engine when sql-native is not
available', () => {
+ WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task']);
+
+ const regularQuery = 'SELECT * FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(regularQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('native');
+ });
+
+ it('falls back to sql-native when no engines are enabled', () => {
+ WorkbenchQuery.setQueryEngines([]);
+
+ const regularQuery = 'SELECT * FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(regularQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+ });
+
+ describe('complex scenarios', () => {
+ it('prioritizes explicit engine over task engine detection', () => {
+ const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia';
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(insertQuery)
+ .changeEngine('sql-native');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('handles SQL query with different enabled engines order', () => {
+ WorkbenchQuery.setQueryEngines(['sql-msq-task', 'native',
'sql-native']);
+
+ const regularQuery = 'SELECT COUNT(*) FROM wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(regularQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('returns sql-msq-task for task query even when sql-native is
enabled', () => {
+ WorkbenchQuery.setQueryEngines(['sql-native', 'sql-msq-task',
'native']);
+
+ const insertQuery = sane`
+ INSERT INTO wiki
+ SELECT * FROM wikipedia
+ PARTITIONED BY DAY
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(insertQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('handles empty query string', () => {
+ const workbenchQuery = WorkbenchQuery.blank();
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('handles query with only whitespace', () => {
+ const workbenchQuery = WorkbenchQuery.blank().changeQueryString('
\n\t ');
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native');
+ });
+
+ it('handles malformed JSON query', () => {
+ const malformedJson = '{ "queryType": "topN"';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(malformedJson);
+
+ // Malformed JSON will be treated as JSON-like (starts with {) but
will fail isSqlInJson check,
+ // falling into the native JSON branch which returns 'native' since
it's enabled
+ expect(workbenchQuery.getEffectiveEngine()).toBe('native');
+ });
+
+ it('correctly identifies case-insensitive INSERT keyword', () => {
+ const insertQuery = 'insert into wiki select * from wikipedia';
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(insertQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+
+ it('correctly identifies case-insensitive EXTERN keyword', () => {
+ const externQuery = sane`
+ SELECT * FROM TABLE(
+ extern(
+ '{"type":"http","uris":["https://example.com/data.json"]}',
+ '{"type":"json"}'
+ )
+ )
+ `;
+
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString(externQuery);
+
+ expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task');
+ });
+ });
+ });
+
+ describe('#getEffectiveContext', () => {
+ describe('for regular SQL queries', () => {
+ it('returns queryContext when no SET statements exist', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString('SELECT * FROM wikipedia')
+ .changeQueryContext({ maxNumTasks: 3, useCache: false });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ useCache: false,
+ });
+ });
+
+ it('merges queryContext with SET statement context', () => {
+ const queryWithSets = sane`
+ SET maxNumTasks = 5;
+ SET timeout = 30000;
+ SELECT * FROM wikipedia
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(queryWithSets)
+ .changeQueryContext({ useCache: false, finalizeAggregations: true });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ useCache: false,
+ finalizeAggregations: true,
+ maxNumTasks: 5,
+ timeout: 30000,
+ });
+ });
+
+ it('prioritizes SET statement context over queryContext', () => {
+ const queryWithSets = sane`
+ SET maxNumTasks = 10;
+ SELECT * FROM wikipedia
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(queryWithSets)
+ .changeQueryContext({ maxNumTasks: 3, useCache: false });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext.maxNumTasks).toBe(10);
+ expect(effectiveContext.useCache).toBe(false);
+ });
+
+ it('handles empty query string', () => {
+ const workbenchQuery = WorkbenchQuery.blank().changeQueryContext({
+ maxNumTasks: 3,
+ });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({ maxNumTasks: 3 });
+ });
+
+ it('handles query with only SET statements', () => {
+ const queryWithOnlySets = sane`
+ SET maxNumTasks = 5;
+ SET useCache = TRUE;
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(queryWithOnlySets)
+ .changeQueryContext({ timeout: 60000 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ timeout: 60000,
+ maxNumTasks: 5,
+ useCache: true,
+ });
+ });
+ });
+
+ describe('for native JSON queries', () => {
+ it('returns queryContext when JSON has no context property', () => {
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia"
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(nativeJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({ maxNumTasks: 3 });
+ });
+
+ it('merges JSON context with queryContext', () => {
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia",
+ "context": {
+ "timeout": 30000,
+ "useCache": false
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(nativeJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ timeout: 30000,
+ useCache: false,
+ });
+ });
+
+ it('prioritizes JSON context over queryContext', () => {
+ const nativeJson = sane`
+ {
+ "queryType": "topN",
+ "dataSource": "wikipedia",
+ "context": {
+ "maxNumTasks": 10
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(nativeJson)
+ .changeQueryContext({ maxNumTasks: 3, useCache: false });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext.maxNumTasks).toBe(10);
+ expect(effectiveContext.useCache).toBe(false);
+ });
+ });
+
+ describe('for SQL-in-JSON queries', () => {
+ it('returns queryContext when JSON has no context and SQL has no SET
statements', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia"
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({ maxNumTasks: 3 });
+ });
+
+ it('merges JSON context with queryContext', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {
+ "timeout": 30000
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ timeout: 30000,
+ });
+ });
+
+ it('merges SQL SET statements context with queryContext', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SET useCache = FALSE; SELECT * FROM wikipedia"
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ useCache: false,
+ });
+ });
+
+ it('merges all three contexts: queryContext, JSON context, and SET
statements', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SET timeout = 60000; SET finalizeAggregations = TRUE;
SELECT * FROM wikipedia",
+ "context": {
+ "maxNumTasks": 5,
+ "useCache": false
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3, priority: 10 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ priority: 10,
+ maxNumTasks: 5,
+ useCache: false,
+ timeout: 60000,
+ finalizeAggregations: true,
+ });
+ });
+
+ it('prioritizes SET statements over JSON context over queryContext', ()
=> {
+ const sqlInJson = sane`
+ {
+ "query": "SET maxNumTasks = 20; SELECT * FROM wikipedia",
+ "context": {
+ "maxNumTasks": 10
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext.maxNumTasks).toBe(20);
+ });
+
+ it('handles SQL-in-JSON with multiple SET statements', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SET maxNumTasks = 8; SET useCache = TRUE; SET timeout =
45000; SELECT * FROM wikipedia",
+ "context": {
+ "finalizeAggregations": false
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ priority: 5 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ priority: 5,
+ finalizeAggregations: false,
+ maxNumTasks: 8,
+ useCache: true,
+ timeout: 45000,
+ });
+ });
+ });
+
+ describe('error handling', () => {
+ it('handles malformed JSON gracefully', () => {
+ const malformedJson = '{ "queryType": "topN"';
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(malformedJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ // Should fall back to queryContext only since JSON parsing fails
+ expect(effectiveContext).toEqual({ maxNumTasks: 3 });
+ });
+
+ it('handles JSON with invalid context property', () => {
+ const jsonWithInvalidContext = sane`
+ {
+ "queryType": "topN",
+ "context": "not an object"
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(jsonWithInvalidContext)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ // Should merge the context even if it's not an object
+ expect(effectiveContext).toBeDefined();
+ });
+
+ it('handles SQL-in-JSON with malformed SET statements', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SET maxNumTasks INVALID; SELECT * FROM wikipedia"
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ // Should still return queryContext even if SET statement is invalid
+ expect(effectiveContext).toBeDefined();
+ expect(effectiveContext.maxNumTasks).toBe(3);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('handles empty queryContext', () => {
+ const workbenchQuery =
WorkbenchQuery.blank().changeQueryString('SELECT * FROM wikipedia');
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({});
+ });
+
+ it('handles whitespace-only query', () => {
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(' \n\t ')
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({ maxNumTasks: 3 });
+ });
+
+ it('handles JSON with null context', () => {
+ const jsonWithNullContext = sane`
+ {
+ "queryType": "topN",
+ "context": null
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(jsonWithNullContext)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toBeDefined();
+ });
+
+ it('handles complex nested context values', () => {
+ const sqlInJson = sane`
+ {
+ "query": "SELECT * FROM wikipedia",
+ "context": {
+ "nestedObject": {
+ "key1": "value1",
+ "key2": 42
+ },
+ "arrayValue": [1, 2, 3]
+ }
+ }
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(sqlInJson)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ nestedObject: {
+ key1: 'value1',
+ key2: 42,
+ },
+ arrayValue: [1, 2, 3],
+ });
+ });
+
+ it('preserves boolean false values in context', () => {
+ const queryWithSets = sane`
+ SET useCache = FALSE;
+ SET finalizeAggregations = FALSE;
+ SELECT * FROM wikipedia
+ `;
+
+ const workbenchQuery = WorkbenchQuery.blank()
+ .changeQueryString(queryWithSets)
+ .changeQueryContext({ maxNumTasks: 3 });
+
+ const effectiveContext = workbenchQuery.getEffectiveContext();
+
+ expect(effectiveContext).toEqual({
+ maxNumTasks: 3,
+ useCache: false,
+ finalizeAggregations: false,
+ });
+ });
+ });
+ });
});
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 825b6af5b15..7072d41f729 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.ts
@@ -309,6 +309,29 @@ export class WorkbenchQuery {
return SqlSetStatement.getContextFromText(this.queryString);
}
+ public getEffectiveContext(): QueryContext {
+ let effectiveContext = this.queryContext;
+ if (this.isJsonLike()) {
+ try {
+ const query = Hjson.parse(this.queryString);
+ effectiveContext = { ...effectiveContext, ...query.context };
+ if (typeof query.query === 'string') {
+ effectiveContext = {
+ ...effectiveContext,
+ ...SqlSetStatement.getContextFromText(query.query),
+ };
+ }
+ } catch {}
+ } else {
+ effectiveContext = {
+ ...effectiveContext,
+ ...SqlSetStatement.getContextFromText(this.queryString),
+ };
+ }
+
+ return effectiveContext;
+ }
+
public changeQueryContext(queryContext: QueryContext): WorkbenchQuery {
return new WorkbenchQuery({ ...this.valueOf(), queryContext });
}
@@ -340,6 +363,12 @@ export class WorkbenchQuery {
public getEffectiveEngine(): DruidEngine {
const { engine } = this;
if (engine) return engine;
+
+ // If an engine is set explicitly in the config then respect it
+ const contextEngine = this.getEffectiveContext().engine;
+ if (contextEngine === 'native') return 'sql-native';
+ if (contextEngine === 'msq-dart') return 'sql-msq-dart';
+
const enabledEngines = WorkbenchQuery.getQueryEngines();
if (this.isJsonLike()) {
if (this.isSqlInJson()) {
@@ -463,7 +492,7 @@ export class WorkbenchQuery {
}
public getMaxNumTasks(): number | undefined {
- return this.getQueryStringContext().maxNumTasks ??
this.queryContext.maxNumTasks;
+ return this.getEffectiveContext().maxNumTasks;
}
public setMaxNumTasksIfUnset(maxNumTasks: number | undefined):
WorkbenchQuery {
@@ -552,13 +581,21 @@ export class WorkbenchQuery {
...queryContext,
};
+ // Effective context lets us see the context which can also include the
set statements from the SetStatements
+ const effectiveContext = {
+ ...apiQuery.context,
+ ...SqlSetStatement.getContextFromText(apiQuery.query || ''),
+ };
+
if (engine === 'sql-native') {
- apiQuery.context.engine ??= 'native';
+ if (typeof effectiveContext.engine === 'undefined') {
+ apiQuery.context.engine = 'native';
+ }
}
let cancelQueryId: string | undefined;
if (engine === 'sql-native' || engine === 'sql-msq-dart') {
- cancelQueryId = apiQuery.context.sqlQueryId;
+ cancelQueryId = effectiveContext.sqlQueryId;
if (!cancelQueryId) {
// If the sqlQueryId is not explicitly set on the context generate
one, so it is possible to cancel the query.
apiQuery.context.sqlQueryId = cancelQueryId = makeQueryId();
@@ -566,22 +603,40 @@ export class WorkbenchQuery {
}
if (engine === 'sql-msq-task') {
- apiQuery.context.executionMode ??= 'async';
+ if (typeof effectiveContext.executionMode === 'undefined') {
+ apiQuery.context.executionMode = 'async';
+ }
+
if (ingestQuery) {
// Alter these defaults for ingest queries if unset
- apiQuery.context.finalizeAggregations ??= false;
- apiQuery.context.groupByEnableMultiValueUnnesting ??= false;
- apiQuery.context.waitUntilSegmentsLoad ??= true;
+ if (typeof effectiveContext.finalizeAggregations === 'undefined') {
+ apiQuery.context.finalizeAggregations = false;
+ }
+ if (typeof effectiveContext.groupByEnableMultiValueUnnesting ===
'undefined') {
+ apiQuery.context.groupByEnableMultiValueUnnesting = false;
+ }
+ if (typeof effectiveContext.waitUntilSegmentsLoad === 'undefined') {
+ apiQuery.context.waitUntilSegmentsLoad = true;
+ }
}
}
if (engine === 'sql-native' || engine === 'sql-msq-task') {
- apiQuery.context.sqlStringifyArrays ??= false;
+ if (typeof effectiveContext.sqlStringifyArrays === 'undefined') {
+ apiQuery.context.sqlStringifyArrays = false;
+ }
}
if (engine === 'sql-msq-dart') {
- apiQuery.context.engine = 'msq-dart';
- apiQuery.context.fullReport ??= true;
+ if (typeof effectiveContext.engine === 'undefined') {
+ apiQuery.context.engine = 'msq-dart';
+ }
+ if (typeof effectiveContext.fullReport === 'undefined') {
+ apiQuery.context.fullReport = true;
+ }
+ if (typeof effectiveContext.liveReportCounters === 'undefined') {
+ apiQuery.context.liveReportCounters = true;
+ }
}
if (Array.isArray(queryParameters) && queryParameters.length) {
diff --git a/web-console/src/utils/query-manager/query-manager.ts
b/web-console/src/utils/query-manager/query-manager.ts
index 5c1a859758c..c802b8014ce 100644
--- a/web-console/src/utils/query-manager/query-manager.ts
+++ b/web-console/src/utils/query-manager/query-manager.ts
@@ -25,12 +25,19 @@ import { IntermediateQueryState } from
'./intermediate-query-state';
import { QueryState } from './query-state';
import { ResultWithAuxiliaryWork } from './result-with-auxiliary-work';
+export interface ProcessQueryExtra<I = never> {
+ setIntermediateQuery: (intermediateQuery: any) => void;
+ setIntermediateStateCallback: (
+ intermediateStateCallback: (signal: AbortSignal) => Promise<I>,
+ ) => void;
+}
+
export interface QueryManagerOptions<Q, R, I = never, E extends Error = Error>
{
initState?: QueryState<R, E, I>;
processQuery: (
query: Q,
signal: AbortSignal,
- setIntermediateQuery: (intermediateQuery: any) => void,
+ extra: ProcessQueryExtra<I>,
) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
backgroundStatusCheck?: (
state: I,
@@ -56,7 +63,7 @@ export class QueryManager<Q, R, I = never, E extends Error =
Error> {
private readonly processQuery: (
query: Q,
signal: AbortSignal,
- setIntermediateQuery: (intermediateQuery: any) => void,
+ extra: ProcessQueryExtra<I>,
) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
private readonly backgroundStatusCheck?: (
@@ -130,8 +137,56 @@ export class QueryManager<Q, R, I = never, E extends Error
= Error> {
const query = this.lastQuery;
let data: R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>;
try {
- data = await this.processQuery(query, signal, (intermediateQuery: any)
=> {
- this.lastIntermediateQuery = intermediateQuery;
+ data = await this.processQuery(query, signal, {
+ setIntermediateQuery: (intermediateQuery: any) => {
+ this.lastIntermediateQuery = intermediateQuery;
+ },
+ setIntermediateStateCallback: intermediateStateCallback => {
+ let backgroundChecks = 0;
+ let intermediateError: Error | undefined;
+
+ void (async () => {
+ while (!signal.aborted && this.currentQueryId === myQueryId) {
+ try {
+ const delay =
+ backgroundChecks > 0
+ ? this.backgroundStatusCheckDelay
+ : this.backgroundStatusCheckInitDelay;
+
+ if (delay) {
+ await wait(delay);
+ if (signal.aborted || this.currentQueryId !== myQueryId)
return;
+ }
+
+ const intermediate = await intermediateStateCallback(signal);
+
+ if (signal.aborted || this.currentQueryId !== myQueryId ||
!this.state.loading) {
+ return;
+ }
+
+ this.setState(
+ new QueryState<R, E, I>({
+ loading: true,
+ intermediate,
+ intermediateError,
+ lastData: this.state.getSomeData(),
+ }),
+ );
+
+ intermediateError = undefined; // Clear the intermediate error
if there was one
+ } catch (e) {
+ if (signal.aborted || this.currentQueryId !== myQueryId)
return;
+ if (this.swallowBackgroundError?.(e)) {
+ intermediateError = e;
+ } else {
+ return; // Stop the loop on unrecoverable error
+ }
+ }
+
+ backgroundChecks++;
+ }
+ })();
+ },
});
} catch (e) {
if (this.currentQueryId !== myQueryId) return;
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 463991a806a..49e0f0e2996 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -435,7 +435,7 @@ GROUP BY 1, 2`;
processQuery: async (
{ capabilities, visibleColumns, showUnused },
signal,
- setIntermediateQuery,
+ { setIntermediateQuery },
) => {
let datasources: DatasourceQueryResultRow[];
if (capabilities.hasSql()) {
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index 67e6bbba569..11e89818d34 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -308,7 +308,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
this.segmentsQueryManager = new QueryManager({
debounceIdle: 500,
- processQuery: async (query: SegmentsQuery, signal, setIntermediateQuery)
=> {
+ processQuery: async (query: SegmentsQuery, signal, {
setIntermediateQuery }) => {
const { page, pageSize, filtered, sorted, visibleColumns,
capabilities, groupByInterval } =
query;
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index 49836246968..294e86d81e9 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -282,7 +282,7 @@ export class SupervisorsView extends React.PureComponent<
processQuery: async (
{ capabilities, visibleColumns, filtered, sorted, page, pageSize },
signal,
- setIntermediateQuery,
+ { setIntermediateQuery },
) => {
let supervisors: SupervisorQueryResultRow[];
let count = -1;
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx
b/web-console/src/views/tasks-view/tasks-view.tsx
index bb064c7dab4..0a866194faa 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -715,6 +715,7 @@ ORDER BY
)}
{executionDialogOpen && (
<ExecutionDetailsDialog
+ type="task"
id={executionDialogOpen}
goToTask={taskId => {
onFiltersChange(TableFilters.eq({ task_id: taskId }));
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
index 2160bbbf654..007e3eca103 100644
---
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
@@ -29,7 +29,6 @@ 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 { getMsqDartVersion, WORK_STATE_STORE } from '../work-state-store';
import './current-dart-panel.scss';
@@ -48,24 +47,22 @@ function stateToIconAndColor(status:
DartQueryEntry['state']): [IconName, string
}
export interface CurrentViberPanelProps {
+ onExecutionDetails(id: string): void;
onClose(): void;
}
export const CurrentDartPanel = React.memo(function CurrentViberPanel(
props: CurrentViberPanelProps,
) {
- const { onClose } = props;
+ const { onExecutionDetails, onClose } = props;
- const [showSql, setShowSql] = useState<string | undefined>();
const [confirmCancelId, setConfirmCancelId] = useState<string | undefined>();
const [dartQueryEntriesState, queryManager] = useQueryManager<number,
DartQueryEntry[]>({
query: useStore(WORK_STATE_STORE, getMsqDartVersion),
processQuery: async (_, signal) => {
- return (
- (await Api.instance.get('/druid/v2/sql/queries', { signal })).data
- .queries as DartQueryEntry[]
- ).filter(q => q.engine === 'msq-dart');
+ return (await Api.instance.get('/druid/v2/sql/queries', { signal })).data
+ .queries as DartQueryEntry[];
},
});
@@ -89,9 +86,9 @@ export const CurrentDartPanel = React.memo(function
CurrentViberPanel(
<Menu>
<MenuItem
icon={IconNames.EYE_OPEN}
- text="Show SQL"
+ text="Show details"
onClick={() => {
- setShowSql(w.sql);
+ onExecutionDetails(w.sqlQueryId);
}}
/>
<MenuItem
@@ -132,10 +129,13 @@ export const CurrentDartPanel = React.memo(function
CurrentViberPanel(
const anonymous = w.identity === 'allowAll' && w.authenticator ===
'allowAll';
return (
<Popover className="work-entry" key={w.sqlQueryId}
position="left" content={menu}>
- <div onDoubleClick={() => setShowSql(w.sql)}>
- <div className="line1">
+ <div onDoubleClick={() => onExecutionDetails(w.sqlQueryId)}>
+ <div
+ className="line1"
+ data-tooltip={`Engine: ${w.engine}\nSQL ID:
${w.sqlQueryId}`}
+ >
<Icon
- className={'status-icon ' + w.state.toLowerCase()}
+ className={`status-icon ${w.state.toLowerCase()}`}
icon={icon}
style={{ color }}
data-tooltip={`State: ${w.state}`}
@@ -187,7 +187,6 @@ export const CurrentDartPanel = React.memo(function
CurrentViberPanel(
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
deleted file mode 100644
index f1f380dc4ec..00000000000
---
a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 0637d6b9644..00000000000
---
a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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-details-dialog/execution-details-dialog.tsx
b/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx
index 8c9de297c6a..9f390fac2ca 100644
---
a/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx
+++
b/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx
@@ -26,6 +26,7 @@ import { ExecutionDetailsPaneLoader } from
'../execution-details-pane-loader/exe
import './execution-details-dialog.scss';
export interface ExecutionDetailsDialogProps {
+ type: 'task' | 'dart';
id: string;
initTab?: ExecutionDetailsTab;
initExecution?: Execution;
@@ -36,12 +37,13 @@ export interface ExecutionDetailsDialogProps {
export const ExecutionDetailsDialog = React.memo(function
ExecutionDetailsDialog(
props: ExecutionDetailsDialogProps,
) {
- const { id, initTab, initExecution, goToTask, onClose } = props;
+ const { type, id, initTab, initExecution, goToTask, onClose } = props;
return (
<Dialog className="execution-details-dialog" isOpen onClose={onClose}
title="Execution details">
<div className={Classes.DIALOG_BODY}>
<ExecutionDetailsPaneLoader
+ type={type}
id={id}
initTab={initTab}
initExecution={initExecution}
diff --git
a/web-console/src/views/workbench-view/execution-details-pane-loader/execution-details-pane-loader.tsx
b/web-console/src/views/workbench-view/execution-details-pane-loader/execution-details-pane-loader.tsx
index 8a06877f6bf..28d201388d5 100644
---
a/web-console/src/views/workbench-view/execution-details-pane-loader/execution-details-pane-loader.tsx
+++
b/web-console/src/views/workbench-view/execution-details-pane-loader/execution-details-pane-loader.tsx
@@ -19,14 +19,25 @@
import React from 'react';
import { Loader } from '../../../components';
-import type { Execution } from '../../../druid-models';
+import { Execution } from '../../../druid-models';
import { getTaskExecution } from '../../../helpers';
import { useInterval, useQueryManager } from '../../../hooks';
+import { Api } from '../../../singletons';
import { QueryState } from '../../../utils';
import type { ExecutionDetailsTab } from
'../execution-details-pane/execution-details-pane';
import { ExecutionDetailsPane } from
'../execution-details-pane/execution-details-pane';
+async function getDartExecution(sqlQueryId: string, signal: AbortSignal):
Promise<Execution> {
+ const { data } = await Api.instance.get(
+ `/druid/v2/sql/queries/${Api.encodePath(sqlQueryId)}/reports`,
+ { signal },
+ );
+
+ return Execution.fromDartReport(data.report).changeSqlQuery(data.query.sql);
+}
+
export interface ExecutionDetailsPaneLoaderProps {
+ type: 'task' | 'dart';
id: string;
initTab?: ExecutionDetailsTab;
initExecution?: Execution;
@@ -36,13 +47,17 @@ export interface ExecutionDetailsPaneLoaderProps {
export const ExecutionDetailsPaneLoader = React.memo(function
ExecutionDetailsPaneLoader(
props: ExecutionDetailsPaneLoaderProps,
) {
- const { id, initTab, initExecution, goToTask } = props;
+ const { type, id, initTab, initExecution, goToTask } = props;
const [executionState, queryManager] = useQueryManager<string, Execution>({
initQuery: initExecution ? undefined : id,
initState: initExecution ? new QueryState({ data: initExecution }) :
undefined,
processQuery: (id, signal) => {
- return getTaskExecution(id, undefined, signal);
+ if (type === 'task') {
+ return getTaskExecution(id, undefined, signal);
+ } else {
+ return getDartExecution(id, signal);
+ }
},
});
@@ -50,7 +65,7 @@ export const ExecutionDetailsPaneLoader = React.memo(function
ExecutionDetailsPa
const execution = executionState.data;
if (!execution) return;
if (execution.isWaitingForQuery()) {
- queryManager.runQuery(execution.id);
+ queryManager.rerunLastQuery();
}
}, 1000);
diff --git
a/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap
b/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap
index 308b030d19f..4a27ae98ca2 100644
---
a/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap
+++
b/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap
@@ -77,8 +77,8 @@ exports[`ExecutionDetailsPane matches snapshot no init tab
1`] = `
>
0:00:04
</Blueprint5.Tag>
+ (starting at
- (starting at
<Blueprint5.Tag
active={false}
fill={false}
diff --git
a/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
b/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
index e59cdb87939..20e5f342eb3 100644
---
a/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
+++
b/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
@@ -80,8 +80,9 @@ export const ExecutionDetailsPane = React.memo(function
ExecutionDetailsPane(
</p>
{execution.startTime && !!execution.duration && (
<p>
- Query took <Tag
minimal>{formatDurationWithMsIfNeeded(execution.duration)}</Tag>{' '}
- (starting at <Tag
minimal>{prettyFormatIsoDate(execution.startTime)}</Tag>)
+ {execution.status === 'RUNNING' ? 'Query is running for ' :
'Query took '}
+ <Tag
minimal>{formatDurationWithMsIfNeeded(execution.duration)}</Tag> (starting at{'
'}
+ <Tag minimal>{prettyFormatIsoDate(execution.startTime)}</Tag>)
</p>
)}
{execution.destination && (
diff --git
a/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx
b/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx
index 4520c20a39b..b8edf41ff69 100644
---
a/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx
+++
b/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx
@@ -49,7 +49,7 @@ export const ExecutionProgressBarPane = React.memo(function
ExecutionProgressBar
}
const idx = stages ? stages.currentStageIndex() : -1;
- const waitingForSegments = stages && !execution.isWaitingForQuery();
+ const waitingForSegments = execution?.isWaitingForSegments();
const segmentStatusDescription = execution?.getSegmentStatusDescription();
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 0bbc26024d9..8ec233b6a81 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
@@ -200,7 +200,7 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
>({
initQuery: cachedExecutionState ? undefined : currentRunningPromise ||
query.getLastExecution(),
initState: cachedExecutionState,
- processQuery: async (q, signal) => {
+ processQuery: async (q, signal, { setIntermediateStateCallback }) => {
if (q instanceof WorkbenchQuery) {
ExecutionStateCache.deleteState(id);
const { engine, query, prefixLines, cancelQueryId } = q.getApiQuery();
@@ -280,6 +280,15 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
.delete(`/druid/v2/sql/${Api.encodePath(cancelQueryId)}`)
.catch(() => {});
});
+
+ setIntermediateStateCallback(async signal => {
+ const { data } = await Api.instance.get(
+
`/druid/v2/sql/queries/${Api.encodePath(cancelQueryId)}/reports`,
+ { signal },
+ );
+
+ return Execution.fromDartReport(data.report);
+ });
}
onQueryChange(props.query.changeLastExecution(undefined));
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx
b/web-console/src/views/workbench-view/workbench-view.tsx
index 16a9f77caae..9fdd778b9c1 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -150,7 +150,12 @@ export interface WorkbenchViewState {
columnMetadataState: QueryState<readonly ColumnMetadata[]>;
- details?: { id: string; initTab?: ExecutionDetailsTab; initExecution?:
Execution };
+ details?: {
+ type: 'task' | 'dart';
+ id: string;
+ initTab?: ExecutionDetailsTab;
+ initExecution?: Execution;
+ };
connectExternalDataDialogOpen: boolean;
explainDialogOpen: boolean;
@@ -293,9 +298,15 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
localStorageSetJson(LocalStorageKeys.WORKBENCH_DART_PANEL, false);
};
- private readonly handleDetailsWithId = (id: string, initTab?:
ExecutionDetailsTab) => {
+ private readonly handleDetailsWithTaskId = (id: string, initTab?:
ExecutionDetailsTab) => {
this.setState({
- details: { id, initTab },
+ details: { type: 'task', id, initTab },
+ });
+ };
+
+ private readonly handleDetailsWithSqlId = (id: string, initTab?:
ExecutionDetailsTab) => {
+ this.setState({
+ details: { type: 'dart', id, initTab },
});
};
@@ -308,7 +319,12 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
initTab?: ExecutionDetailsTab,
) => {
this.setState({
- details: { id: execution.id, initExecution: execution, initTab },
+ details: {
+ type: execution.engine === 'sql-msq-dart' ? 'dart' : 'task',
+ id: execution.id,
+ initExecution: execution,
+ initTab,
+ },
});
};
@@ -344,6 +360,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
return (
<ExecutionDetailsDialog
+ type={details.type}
id={details.id}
initTab={details.initTab}
initExecution={details.initExecution}
@@ -488,6 +505,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
onSubmit={execution => {
this.setState({
details: {
+ type: 'task',
id: execution.id,
initExecution: execution,
},
@@ -968,13 +986,16 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
{showRecentQueryTaskPanel && (
<RecentQueryTaskPanel
onClose={this.handleRecentQueryTaskPanelClose}
- onExecutionDetails={this.handleDetailsWithId}
+ onExecutionDetails={this.handleDetailsWithTaskId}
onChangeQuery={this.handleQueryStringChange}
onNewTab={this.handleNewTab}
/>
)}
{showCurrentDartPanel && (
- <CurrentDartPanel onClose={this.handleCurrentDartPanelClose} />
+ <CurrentDartPanel
+ onClose={this.handleCurrentDartPanelClose}
+ onExecutionDetails={this.handleDetailsWithSqlId}
+ />
)}
</div>
)}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]