mirror of https://github.com/apache/druid.git
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.
This commit is contained in:
parent
ffa553593f
commit
1f95a42bb8
|
@ -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', () => {
|
||||
|
|
|
@ -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<T = any>(sqlQueryPayload: Record<string, any
|
|||
return sqlResultResp.data;
|
||||
}
|
||||
|
||||
export interface BasicQueryExplanation {
|
||||
export interface QueryExplanation {
|
||||
query: any;
|
||||
signature: string | null;
|
||||
signature: { name: string; type: string }[];
|
||||
}
|
||||
|
||||
export interface SemiJoinQueryExplanation {
|
||||
mainQuery: BasicQueryExplanation;
|
||||
subQueryRight: BasicQueryExplanation;
|
||||
}
|
||||
|
||||
function parseQueryPlanResult(queryPlanResult: string): BasicQueryExplanation {
|
||||
if (!queryPlanResult) {
|
||||
return {
|
||||
query: null,
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
|
||||
const queryAndSignature = queryPlanResult.split(', signature=');
|
||||
const queryValue = new RegExp(/query=(.+)/).exec(queryAndSignature[0]);
|
||||
const signatureValue = queryAndSignature[1];
|
||||
|
||||
let parsedQuery: any;
|
||||
|
||||
if (queryValue && queryValue[1]) {
|
||||
try {
|
||||
parsedQuery = JSON.parse(queryValue[1]);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return {
|
||||
query: parsedQuery || queryPlanResult,
|
||||
signature: signatureValue || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseQueryPlan(
|
||||
raw: string,
|
||||
): BasicQueryExplanation | SemiJoinQueryExplanation | string {
|
||||
let plan: string = raw;
|
||||
plan = plan.replace(/\n/g, '');
|
||||
|
||||
if (plan.includes('DruidOuterQueryRel(')) {
|
||||
return plan; // don't know how to parse this
|
||||
}
|
||||
|
||||
let queryArgs: string;
|
||||
const queryRelFnStart = 'DruidQueryRel(';
|
||||
const semiJoinFnStart = 'DruidSemiJoin(';
|
||||
|
||||
if (plan.startsWith(queryRelFnStart)) {
|
||||
queryArgs = plan.substring(queryRelFnStart.length, plan.length - 1);
|
||||
} else if (plan.startsWith(semiJoinFnStart)) {
|
||||
queryArgs = plan.substring(semiJoinFnStart.length, plan.length - 1);
|
||||
const leftExpressionsArgs = ', leftExpressions=';
|
||||
const keysArgumentIdx = queryArgs.indexOf(leftExpressionsArgs);
|
||||
if (keysArgumentIdx !== -1) {
|
||||
return {
|
||||
mainQuery: parseQueryPlanResult(queryArgs.substring(0, keysArgumentIdx)),
|
||||
subQueryRight: parseQueryPlan(queryArgs.substring(queryArgs.indexOf(queryRelFnStart))),
|
||||
} as SemiJoinQueryExplanation;
|
||||
}
|
||||
} else {
|
||||
return plan;
|
||||
}
|
||||
|
||||
return parseQueryPlanResult(queryArgs);
|
||||
export function formatSignature(queryExplanation: QueryExplanation): string {
|
||||
return queryExplanation.signature
|
||||
.map(({ name, type }) => `${SqlRef.column(name)}::${type}`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function trimSemicolon(query: string): string {
|
||||
|
|
|
@ -86,7 +86,175 @@ exports[`ExplainDialog matches snapshot on loading 1`] = `
|
|||
</Blueprint3.Dialog>
|
||||
`;
|
||||
|
||||
exports[`ExplainDialog matches snapshot on some data 1`] = `
|
||||
exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = `
|
||||
<Blueprint3.Dialog
|
||||
canOutsideClickClose={true}
|
||||
className="explain-dialog"
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
title="Query plan"
|
||||
>
|
||||
<div
|
||||
className="bp3-dialog-body"
|
||||
>
|
||||
<Blueprint3.Tabs
|
||||
animate={true}
|
||||
large={false}
|
||||
renderActiveTabPanelOnly={true}
|
||||
vertical={true}
|
||||
>
|
||||
<Blueprint3.Tab
|
||||
disabled={false}
|
||||
id={0}
|
||||
key="0"
|
||||
panel={
|
||||
<div
|
||||
className="query-explanation"
|
||||
>
|
||||
<Blueprint3.FormGroup
|
||||
className="query-group"
|
||||
label="Query"
|
||||
>
|
||||
<Blueprint3.TextArea
|
||||
readOnly={true}
|
||||
value="{
|
||||
\\"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\\"
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.FormGroup
|
||||
className="signature-group"
|
||||
label="Signature"
|
||||
>
|
||||
<Blueprint3.InputGroup
|
||||
defaultValue="channel::STRING"
|
||||
readOnly={true}
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.Button
|
||||
className="open-query"
|
||||
intent="primary"
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
rightIcon="arrow-top-right"
|
||||
text="Open query"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
title="Query 1"
|
||||
/>
|
||||
<Blueprint3.Tab
|
||||
disabled={false}
|
||||
id={1}
|
||||
key="1"
|
||||
panel={
|
||||
<div
|
||||
className="query-explanation"
|
||||
>
|
||||
<Blueprint3.FormGroup
|
||||
className="query-group"
|
||||
label="Query"
|
||||
>
|
||||
<Blueprint3.TextArea
|
||||
readOnly={true}
|
||||
value="{
|
||||
\\"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\\"
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.FormGroup
|
||||
className="signature-group"
|
||||
label="Signature"
|
||||
>
|
||||
<Blueprint3.InputGroup
|
||||
defaultValue="channel::STRING"
|
||||
readOnly={true}
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.Button
|
||||
className="open-query"
|
||||
intent="primary"
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
rightIcon="arrow-top-right"
|
||||
text="Open query"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
title="Query 2"
|
||||
/>
|
||||
<Expander />
|
||||
</Blueprint3.Tabs>
|
||||
</div>
|
||||
<div
|
||||
className="bp3-dialog-footer"
|
||||
>
|
||||
<div
|
||||
className="bp3-dialog-footer-actions"
|
||||
>
|
||||
<Blueprint3.Button
|
||||
onClick={[Function]}
|
||||
text="Close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Blueprint3.Dialog>
|
||||
`;
|
||||
|
||||
exports[`ExplainDialog matches snapshot on some data (one query) 1`] = `
|
||||
<Blueprint3.Dialog
|
||||
canOutsideClickClose={true}
|
||||
className="explain-dialog"
|
||||
|
@ -98,9 +266,10 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
|||
className="bp3-dialog-body"
|
||||
>
|
||||
<div
|
||||
className="one-query"
|
||||
className="query-explanation"
|
||||
>
|
||||
<Blueprint3.FormGroup
|
||||
className="query-group"
|
||||
label="Query"
|
||||
>
|
||||
<Blueprint3.TextArea
|
||||
|
@ -108,19 +277,71 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
|||
value="{
|
||||
\\"queryType\\": \\"topN\\",
|
||||
\\"dataSource\\": {
|
||||
\\"type\\": \\"table\\",
|
||||
\\"name\\": \\"kttm-multi-day\\"
|
||||
\\"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\\": \\"browser\\",
|
||||
\\"dimension\\": \\"channel\\",
|
||||
\\"outputName\\": \\"d0\\",
|
||||
\\"outputType\\": \\"STRING\\"
|
||||
},
|
||||
\\"metric\\": {
|
||||
\\"type\\": \\"numeric\\",
|
||||
\\"metric\\": \\"a0\\"
|
||||
\\"type\\": \\"dimension\\",
|
||||
\\"previousStop\\": null,
|
||||
\\"ordering\\": {
|
||||
\\"type\\": \\"lexicographic\\"
|
||||
}
|
||||
},
|
||||
\\"threshold\\": 101,
|
||||
\\"intervals\\": {
|
||||
|
@ -140,22 +361,28 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
|||
}
|
||||
],
|
||||
\\"postAggregations\\": [],
|
||||
\\"context\\": {
|
||||
\\"sqlOuterLimit\\": 101,
|
||||
\\"sqlQueryId\\": \\"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6\\"
|
||||
},
|
||||
\\"context\\": {},
|
||||
\\"descending\\": false
|
||||
}"
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.FormGroup
|
||||
className="signature-group"
|
||||
label="Signature"
|
||||
>
|
||||
<Blueprint3.InputGroup
|
||||
defaultValue="[{d0:STRING, a0:LONG}]"
|
||||
defaultValue="d0::STRING, a0::LONG"
|
||||
readOnly={true}
|
||||
/>
|
||||
</Blueprint3.FormGroup>
|
||||
<Blueprint3.Button
|
||||
className="open-query"
|
||||
intent="primary"
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
rightIcon="arrow-top-right"
|
||||
text="Open query"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -168,11 +395,6 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
|||
onClick={[Function]}
|
||||
text="Close"
|
||||
/>
|
||||
<Blueprint3.Button
|
||||
intent="primary"
|
||||
onClick={[Function]}
|
||||
text="Open query"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Blueprint3.Dialog>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<BasicQueryExplanation | SemiJoinQueryExplanation | string> =
|
||||
QueryState.INIT;
|
||||
let explainState: QueryState<QueryExplanation[] | string> = 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();
|
||||
|
|
|
@ -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<QueryWithContext, QueryExplanation[] | string>({
|
||||
processQuery: async (queryWithContext: QueryWithContext) => {
|
||||
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
|
||||
|
||||
let context: Record<string, any> | 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 (
|
||||
<div className="query-explanation">
|
||||
<FormGroup className="query-group" label="Query">
|
||||
<TextArea readOnly value={queryString} />
|
||||
</FormGroup>
|
||||
<FormGroup className="signature-group" label="Signature">
|
||||
<InputGroup defaultValue={formatSignature(queryExplanation)} readOnly />
|
||||
</FormGroup>
|
||||
<Button
|
||||
className="open-query"
|
||||
text="Open query"
|
||||
rightIcon={IconNames.ARROW_TOP_RIGHT}
|
||||
intent={Intent.PRIMARY}
|
||||
minimal
|
||||
onClick={() => {
|
||||
setQueryString(queryString);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
content = <Loader />;
|
||||
} else if (explainError) {
|
||||
content = <div>{explainError.message}</div>;
|
||||
} else if (!explainResult) {
|
||||
content = <div />;
|
||||
} else if ((explainResult as BasicQueryExplanation).query) {
|
||||
queryString = JSONBig.stringify(
|
||||
(explainResult as BasicQueryExplanation).query[0],
|
||||
undefined,
|
||||
2,
|
||||
);
|
||||
content = (
|
||||
<div className="one-query">
|
||||
<FormGroup label="Query">
|
||||
<TextArea readOnly value={queryString} />
|
||||
</FormGroup>
|
||||
{(explainResult as BasicQueryExplanation).signature && (
|
||||
<FormGroup label="Signature">
|
||||
<InputGroup
|
||||
defaultValue={(explainResult as BasicQueryExplanation).signature || ''}
|
||||
readOnly
|
||||
} else if (Array.isArray(explainResult) && explainResult.length) {
|
||||
if (explainResult.length === 1) {
|
||||
content = renderQueryExplanation(explainResult[0]);
|
||||
} else {
|
||||
content = (
|
||||
<Tabs animate renderActiveTabPanelOnly vertical>
|
||||
{explainResult.map((queryExplanation, i) => (
|
||||
<Tab
|
||||
id={i}
|
||||
key={i}
|
||||
title={`Query ${i + 1}`}
|
||||
panel={renderQueryExplanation(queryExplanation)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
(explainResult as SemiJoinQueryExplanation).mainQuery &&
|
||||
(explainResult as SemiJoinQueryExplanation).subQueryRight
|
||||
) {
|
||||
content = (
|
||||
<div className="two-queries">
|
||||
<FormGroup label="Main query">
|
||||
<TextArea
|
||||
readOnly
|
||||
value={JSONBig.stringify(
|
||||
(explainResult as SemiJoinQueryExplanation).mainQuery.query,
|
||||
undefined,
|
||||
2,
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{(explainResult as SemiJoinQueryExplanation).mainQuery.signature && (
|
||||
<FormGroup label="Signature">
|
||||
<InputGroup
|
||||
defaultValue={(explainResult as SemiJoinQueryExplanation).mainQuery.signature || ''}
|
||||
readOnly
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup label="Sub query">
|
||||
<TextArea
|
||||
readOnly
|
||||
value={JSONBig.stringify(
|
||||
(explainResult as SemiJoinQueryExplanation).subQueryRight.query,
|
||||
undefined,
|
||||
2,
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{(explainResult as SemiJoinQueryExplanation).subQueryRight.signature && (
|
||||
<FormGroup label="Signature">
|
||||
<InputGroup
|
||||
defaultValue={
|
||||
(explainResult as SemiJoinQueryExplanation).subQueryRight.signature || ''
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
))}
|
||||
<Tabs.Expander />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
content = <div className="generic-result">{explainResult}</div>;
|
||||
}
|
||||
|
@ -180,16 +168,6 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia
|
|||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button text="Close" onClick={onClose} />
|
||||
{queryString && (
|
||||
<Button
|
||||
text="Open query"
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => {
|
||||
if (queryString) setQueryString(queryString);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
|
Loading…
Reference in New Issue