From 1f95a42bb8b32c314934e8e2ecdeb6569c7e8db3 Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Wed, 1 Dec 2021 10:48:11 -0800 Subject: [PATCH] Web console: updated the explain dialog to use new explain output (#12009) This is the UI followup to the work done in #11908 Updated the Explain dialog to use the new output format. --- web-console/src/utils/druid-query.spec.ts | 12 +- web-console/src/utils/druid-query.ts | 71 +---- .../explain-dialog.spec.tsx.snap | 256 ++++++++++++++++-- .../explain-dialog/explain-dialog.scss | 60 ++-- .../explain-dialog/explain-dialog.spec.tsx | 191 ++++++++++++- .../explain-dialog/explain-dialog.tsx | 150 +++++----- 6 files changed, 532 insertions(+), 208 deletions(-) diff --git a/web-console/src/utils/druid-query.spec.ts b/web-console/src/utils/druid-query.spec.ts index 2a6835da92d..d3b3a1321a3 100644 --- a/web-console/src/utils/druid-query.spec.ts +++ b/web-console/src/utils/druid-query.spec.ts @@ -18,13 +18,7 @@ import { sane } from 'druid-query-toolkit/build/test-utils'; -import { - DruidError, - getDruidErrorMessage, - parseHtmlError, - parseQueryPlan, - trimSemicolon, -} from './druid-query'; +import { DruidError, getDruidErrorMessage, parseHtmlError, trimSemicolon } from './druid-query'; describe('DruidQuery', () => { describe('DruidError.parsePosition', () => { @@ -227,10 +221,6 @@ describe('DruidQuery', () => { it('parseHtmlError', () => { expect(getDruidErrorMessage({})).toMatchInlineSnapshot(`undefined`); }); - - it('parseQueryPlan', () => { - expect(parseQueryPlan('start')).toMatchInlineSnapshot(`"start"`); - }); }); describe('.trimSemicolon', () => { diff --git a/web-console/src/utils/druid-query.ts b/web-console/src/utils/druid-query.ts index e3e958b0296..b9ac79390ce 100644 --- a/web-console/src/utils/druid-query.ts +++ b/web-console/src/utils/druid-query.ts @@ -17,6 +17,7 @@ */ import axios, { AxiosResponse } from 'axios'; +import { SqlRef } from 'druid-query-toolkit'; import { Api } from '../singletons'; @@ -296,73 +297,15 @@ export async function queryDruidSql(sqlQueryPayload: Record `${SqlRef.column(name)}::${type}`) + .join(', '); } export function trimSemicolon(query: string): string { diff --git a/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap b/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap index ba424eb8d34..36df368d99e 100644 --- a/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap +++ b/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap @@ -86,7 +86,175 @@ exports[`ExplainDialog matches snapshot on loading 1`] = ` `; -exports[`ExplainDialog matches snapshot on some data 1`] = ` +exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = ` + +
+ + + + + + + + + +
+ } + title="Query 1" + /> + + + + + + + + + + } + title="Query 2" + /> + + + +
+
+ +
+
+
+`; + +exports[`ExplainDialog matches snapshot on some data (one query) 1`] = `
+
-
diff --git a/web-console/src/views/query-view/explain-dialog/explain-dialog.scss b/web-console/src/views/query-view/explain-dialog/explain-dialog.scss index de1a088d1cc..e447fdf6d85 100644 --- a/web-console/src/views/query-view/explain-dialog/explain-dialog.scss +++ b/web-console/src/views/query-view/explain-dialog/explain-dialog.scss @@ -18,30 +18,54 @@ .explain-dialog { &.bp3-dialog { - width: 600px; + width: 800px; + height: 90vh; } .bp3-dialog-body { - min-height: 70vh; - } - - textarea { - width: 100%; - } - - .one-query { - textarea { - height: 50vh !important; + .bp3-tabs { + width: 100%; + height: 100%; } - } - .two-queries { - textarea { - height: 25vh !important; + .bp3-tab-panel { + flex: 1; } - } - .generic-result { - overflow: scroll; + .query-explanation { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + + textarea { + width: 100%; + height: 100%; + resize: none; + } + + .query-group { + flex: 1; + + .bp3-form-content { + flex: 1; + } + } + + .signature-group { + margin-bottom: 0; + } + + .open-query { + position: absolute; + top: 30px; + right: 6px; + } + } + + .generic-result { + overflow: scroll; + white-space: pre-wrap; + } } } diff --git a/web-console/src/views/query-view/explain-dialog/explain-dialog.spec.tsx b/web-console/src/views/query-view/explain-dialog/explain-dialog.spec.tsx index 2109e5ed2a0..ab5f046c820 100644 --- a/web-console/src/views/query-view/explain-dialog/explain-dialog.spec.tsx +++ b/web-console/src/views/query-view/explain-dialog/explain-dialog.spec.tsx @@ -19,17 +19,11 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { - BasicQueryExplanation, - parseQueryPlan, - QueryState, - SemiJoinQueryExplanation, -} from '../../../utils'; +import { QueryExplanation, QueryState } from '../../../utils'; import { ExplainDialog } from './explain-dialog'; -let explainState: QueryState = - QueryState.INIT; +let explainState: QueryState = QueryState.INIT; jest.mock('../../../hooks', () => { return { @@ -64,11 +58,184 @@ describe('ExplainDialog', () => { expect(shallow(makeExplainDialog())).toMatchSnapshot(); }); - it('matches snapshot on some data', () => { + it('matches snapshot on some data (one query)', () => { explainState = new QueryState({ - data: parseQueryPlan( - `DruidQueryRel(query=[{"queryType":"topN","dataSource":{"type":"table","name":"kttm-multi-day"},"virtualColumns":[],"dimension":{"type":"default","dimension":"browser","outputName":"d0","outputType":"STRING"},"metric":{"type":"numeric","metric":"a0"},"threshold":101,"intervals":{"type":"intervals","intervals":["-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z"]},"filter":null,"granularity":{"type":"all"},"aggregations":[{"type":"count","name":"a0"}],"postAggregations":[],"context":{"sqlOuterLimit":101,"sqlQueryId":"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6"},"descending":false}], signature=[{d0:STRING, a0:LONG}])`, - ), + data: [ + { + query: { + queryType: 'topN', + dataSource: { + type: 'join', + left: { + type: 'table', + name: 'wikipedia', + }, + right: { + type: 'query', + query: { + queryType: 'groupBy', + dataSource: { + type: 'table', + name: 'wikipedia', + }, + intervals: { + type: 'intervals', + intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'], + }, + virtualColumns: [], + filter: { + type: 'selector', + dimension: 'channel', + value: '#en.wikipedia', + extractionFn: null, + }, + granularity: { + type: 'all', + }, + dimensions: [ + { + type: 'default', + dimension: 'channel', + outputName: 'd0', + outputType: 'STRING', + }, + ], + aggregations: [], + postAggregations: [], + having: null, + limitSpec: { + type: 'NoopLimitSpec', + }, + context: {}, + descending: false, + }, + }, + rightPrefix: 'j0.', + condition: '("channel" == "j0.d0")', + joinType: 'LEFT', + leftFilter: null, + }, + virtualColumns: [], + dimension: { + type: 'default', + dimension: 'channel', + outputName: 'd0', + outputType: 'STRING', + }, + metric: { + type: 'dimension', + previousStop: null, + ordering: { + type: 'lexicographic', + }, + }, + threshold: 101, + intervals: { + type: 'intervals', + intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'], + }, + filter: null, + granularity: { + type: 'all', + }, + aggregations: [ + { + type: 'count', + name: 'a0', + }, + ], + postAggregations: [], + context: {}, + descending: false, + }, + signature: [ + { + name: 'd0', + type: 'STRING', + }, + { + name: 'a0', + type: 'LONG', + }, + ], + }, + ], + }); + + expect(shallow(makeExplainDialog())).toMatchSnapshot(); + }); + + it('matches snapshot on some data (many queries)', () => { + explainState = new QueryState({ + data: [ + { + query: { + queryType: 'scan', + dataSource: { + type: 'table', + name: 'wikipedia', + }, + intervals: { + type: 'intervals', + intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'], + }, + virtualColumns: [], + resultFormat: 'compactedList', + batchSize: 20480, + limit: 101, + filter: null, + columns: ['channel'], + legacy: false, + context: {}, + descending: false, + granularity: { + type: 'all', + }, + }, + signature: [ + { + name: 'channel', + type: 'STRING', + }, + ], + }, + { + query: { + queryType: 'scan', + dataSource: { + type: 'table', + name: 'wikipedia', + }, + intervals: { + type: 'intervals', + intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'], + }, + virtualColumns: [], + resultFormat: 'compactedList', + batchSize: 20480, + limit: 101, + filter: { + type: 'selector', + dimension: 'channel', + value: '#en.wikipedia', + extractionFn: null, + }, + columns: ['channel'], + legacy: false, + context: {}, + descending: false, + granularity: { + type: 'all', + }, + }, + signature: [ + { + name: 'channel', + type: 'STRING', + }, + ], + }, + ], }); expect(shallow(makeExplainDialog())).toMatchSnapshot(); diff --git a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx index db4713de784..d1b4301c644 100644 --- a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx +++ b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx @@ -23,20 +23,22 @@ import { FormGroup, InputGroup, Intent, + Tab, + Tabs, TextArea, } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import * as JSONBig from 'json-bigint-native'; import React from 'react'; import { Loader } from '../../../components'; import { useQueryManager } from '../../../hooks'; import { - BasicQueryExplanation, + formatSignature, getDruidErrorMessage, - parseQueryPlan, queryDruidSql, + QueryExplanation, QueryWithContext, - SemiJoinQueryExplanation, trimSemicolon, } from '../../../utils'; import { isEmptyContext } from '../../../utils/query-context'; @@ -63,16 +65,17 @@ export interface ExplainDialogProps { export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) { const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props; - const [explainState] = useQueryManager< - QueryWithContext, - BasicQueryExplanation | SemiJoinQueryExplanation | string - >({ + const [explainState] = useQueryManager({ processQuery: async (queryWithContext: QueryWithContext) => { const { queryString, queryContext, wrapQueryLimit } = queryWithContext; let context: Record | undefined; if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) { - context = { ...queryContext, ...(mandatoryQueryContext || {}) }; + context = { + ...queryContext, + ...(mandatoryQueryContext || {}), + useNativeQueryExplain: true, + }; if (typeof wrapQueryLimit !== 'undefined') { context.sqlOuterLimit = wrapQueryLimit + 1; } @@ -88,88 +91,73 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia throw new Error(getDruidErrorMessage(e)); } - return parseQueryPlan(result[0]['PLAN']); + const plan = result[0]['PLAN']; + if (typeof plan !== 'string') { + throw new Error(`unexpected result from server`); + } + + try { + return JSONBig.parse(plan); + } catch { + return plan; + } }, initQuery: queryWithContext, }); let content: JSX.Element; - let queryString: string | undefined; const { loading, error: explainError, data: explainResult } = explainState; + + function renderQueryExplanation(queryExplanation: QueryExplanation) { + const queryString = JSONBig.stringify(queryExplanation.query, undefined, 2); + return ( +
+ +