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:
Vadim Ogievetsky 2021-12-01 10:48:11 -08:00 committed by GitHub
parent ffa553593f
commit 1f95a42bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 532 additions and 208 deletions

View File

@ -18,13 +18,7 @@
import { sane } from 'druid-query-toolkit/build/test-utils'; import { sane } from 'druid-query-toolkit/build/test-utils';
import { import { DruidError, getDruidErrorMessage, parseHtmlError, trimSemicolon } from './druid-query';
DruidError,
getDruidErrorMessage,
parseHtmlError,
parseQueryPlan,
trimSemicolon,
} from './druid-query';
describe('DruidQuery', () => { describe('DruidQuery', () => {
describe('DruidError.parsePosition', () => { describe('DruidError.parsePosition', () => {
@ -227,10 +221,6 @@ describe('DruidQuery', () => {
it('parseHtmlError', () => { it('parseHtmlError', () => {
expect(getDruidErrorMessage({})).toMatchInlineSnapshot(`undefined`); expect(getDruidErrorMessage({})).toMatchInlineSnapshot(`undefined`);
}); });
it('parseQueryPlan', () => {
expect(parseQueryPlan('start')).toMatchInlineSnapshot(`"start"`);
});
}); });
describe('.trimSemicolon', () => { describe('.trimSemicolon', () => {

View File

@ -17,6 +17,7 @@
*/ */
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { SqlRef } from 'druid-query-toolkit';
import { Api } from '../singletons'; import { Api } from '../singletons';
@ -296,73 +297,15 @@ export async function queryDruidSql<T = any>(sqlQueryPayload: Record<string, any
return sqlResultResp.data; return sqlResultResp.data;
} }
export interface BasicQueryExplanation { export interface QueryExplanation {
query: any; query: any;
signature: string | null; signature: { name: string; type: string }[];
} }
export interface SemiJoinQueryExplanation { export function formatSignature(queryExplanation: QueryExplanation): string {
mainQuery: BasicQueryExplanation; return queryExplanation.signature
subQueryRight: BasicQueryExplanation; .map(({ name, type }) => `${SqlRef.column(name)}::${type}`)
} .join(', ');
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 trimSemicolon(query: string): string { export function trimSemicolon(query: string): string {

View File

@ -86,7 +86,175 @@ exports[`ExplainDialog matches snapshot on loading 1`] = `
</Blueprint3.Dialog> </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 <Blueprint3.Dialog
canOutsideClickClose={true} canOutsideClickClose={true}
className="explain-dialog" className="explain-dialog"
@ -98,9 +266,10 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
className="bp3-dialog-body" className="bp3-dialog-body"
> >
<div <div
className="one-query" className="query-explanation"
> >
<Blueprint3.FormGroup <Blueprint3.FormGroup
className="query-group"
label="Query" label="Query"
> >
<Blueprint3.TextArea <Blueprint3.TextArea
@ -108,19 +277,71 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
value="{ value="{
\\"queryType\\": \\"topN\\", \\"queryType\\": \\"topN\\",
\\"dataSource\\": { \\"dataSource\\": {
\\"type\\": \\"table\\", \\"type\\": \\"join\\",
\\"name\\": \\"kttm-multi-day\\" \\"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\\": [], \\"virtualColumns\\": [],
\\"dimension\\": { \\"dimension\\": {
\\"type\\": \\"default\\", \\"type\\": \\"default\\",
\\"dimension\\": \\"browser\\", \\"dimension\\": \\"channel\\",
\\"outputName\\": \\"d0\\", \\"outputName\\": \\"d0\\",
\\"outputType\\": \\"STRING\\" \\"outputType\\": \\"STRING\\"
}, },
\\"metric\\": { \\"metric\\": {
\\"type\\": \\"numeric\\", \\"type\\": \\"dimension\\",
\\"metric\\": \\"a0\\" \\"previousStop\\": null,
\\"ordering\\": {
\\"type\\": \\"lexicographic\\"
}
}, },
\\"threshold\\": 101, \\"threshold\\": 101,
\\"intervals\\": { \\"intervals\\": {
@ -140,22 +361,28 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
} }
], ],
\\"postAggregations\\": [], \\"postAggregations\\": [],
\\"context\\": { \\"context\\": {},
\\"sqlOuterLimit\\": 101,
\\"sqlQueryId\\": \\"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6\\"
},
\\"descending\\": false \\"descending\\": false
}" }"
/> />
</Blueprint3.FormGroup> </Blueprint3.FormGroup>
<Blueprint3.FormGroup <Blueprint3.FormGroup
className="signature-group"
label="Signature" label="Signature"
> >
<Blueprint3.InputGroup <Blueprint3.InputGroup
defaultValue="[{d0:STRING, a0:LONG}]" defaultValue="d0::STRING, a0::LONG"
readOnly={true} readOnly={true}
/> />
</Blueprint3.FormGroup> </Blueprint3.FormGroup>
<Blueprint3.Button
className="open-query"
intent="primary"
minimal={true}
onClick={[Function]}
rightIcon="arrow-top-right"
text="Open query"
/>
</div> </div>
</div> </div>
<div <div
@ -168,11 +395,6 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
onClick={[Function]} onClick={[Function]}
text="Close" text="Close"
/> />
<Blueprint3.Button
intent="primary"
onClick={[Function]}
text="Open query"
/>
</div> </div>
</div> </div>
</Blueprint3.Dialog> </Blueprint3.Dialog>

View File

@ -18,30 +18,54 @@
.explain-dialog { .explain-dialog {
&.bp3-dialog { &.bp3-dialog {
width: 600px; width: 800px;
height: 90vh;
} }
.bp3-dialog-body { .bp3-dialog-body {
min-height: 70vh; .bp3-tabs {
} width: 100%;
height: 100%;
textarea {
width: 100%;
}
.one-query {
textarea {
height: 50vh !important;
} }
}
.two-queries { .bp3-tab-panel {
textarea { flex: 1;
height: 25vh !important;
} }
}
.generic-result { .query-explanation {
overflow: scroll; 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;
}
} }
} }

View File

@ -19,17 +19,11 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import { import { QueryExplanation, QueryState } from '../../../utils';
BasicQueryExplanation,
parseQueryPlan,
QueryState,
SemiJoinQueryExplanation,
} from '../../../utils';
import { ExplainDialog } from './explain-dialog'; import { ExplainDialog } from './explain-dialog';
let explainState: QueryState<BasicQueryExplanation | SemiJoinQueryExplanation | string> = let explainState: QueryState<QueryExplanation[] | string> = QueryState.INIT;
QueryState.INIT;
jest.mock('../../../hooks', () => { jest.mock('../../../hooks', () => {
return { return {
@ -64,11 +58,184 @@ describe('ExplainDialog', () => {
expect(shallow(makeExplainDialog())).toMatchSnapshot(); expect(shallow(makeExplainDialog())).toMatchSnapshot();
}); });
it('matches snapshot on some data', () => { it('matches snapshot on some data (one query)', () => {
explainState = new QueryState({ explainState = new QueryState({
data: parseQueryPlan( data: [
`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}])`, {
), 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(); expect(shallow(makeExplainDialog())).toMatchSnapshot();

View File

@ -23,20 +23,22 @@ import {
FormGroup, FormGroup,
InputGroup, InputGroup,
Intent, Intent,
Tab,
Tabs,
TextArea, TextArea,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import * as JSONBig from 'json-bigint-native'; import * as JSONBig from 'json-bigint-native';
import React from 'react'; import React from 'react';
import { Loader } from '../../../components'; import { Loader } from '../../../components';
import { useQueryManager } from '../../../hooks'; import { useQueryManager } from '../../../hooks';
import { import {
BasicQueryExplanation, formatSignature,
getDruidErrorMessage, getDruidErrorMessage,
parseQueryPlan,
queryDruidSql, queryDruidSql,
QueryExplanation,
QueryWithContext, QueryWithContext,
SemiJoinQueryExplanation,
trimSemicolon, trimSemicolon,
} from '../../../utils'; } from '../../../utils';
import { isEmptyContext } from '../../../utils/query-context'; import { isEmptyContext } from '../../../utils/query-context';
@ -63,16 +65,17 @@ export interface ExplainDialogProps {
export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) { export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) {
const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props; const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props;
const [explainState] = useQueryManager< const [explainState] = useQueryManager<QueryWithContext, QueryExplanation[] | string>({
QueryWithContext,
BasicQueryExplanation | SemiJoinQueryExplanation | string
>({
processQuery: async (queryWithContext: QueryWithContext) => { processQuery: async (queryWithContext: QueryWithContext) => {
const { queryString, queryContext, wrapQueryLimit } = queryWithContext; const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
let context: Record<string, any> | undefined; let context: Record<string, any> | undefined;
if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) { if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) {
context = { ...queryContext, ...(mandatoryQueryContext || {}) }; context = {
...queryContext,
...(mandatoryQueryContext || {}),
useNativeQueryExplain: true,
};
if (typeof wrapQueryLimit !== 'undefined') { if (typeof wrapQueryLimit !== 'undefined') {
context.sqlOuterLimit = wrapQueryLimit + 1; context.sqlOuterLimit = wrapQueryLimit + 1;
} }
@ -88,88 +91,73 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia
throw new Error(getDruidErrorMessage(e)); 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, initQuery: queryWithContext,
}); });
let content: JSX.Element; let content: JSX.Element;
let queryString: string | undefined;
const { loading, error: explainError, data: explainResult } = explainState; 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) { if (loading) {
content = <Loader />; content = <Loader />;
} else if (explainError) { } else if (explainError) {
content = <div>{explainError.message}</div>; content = <div>{explainError.message}</div>;
} else if (!explainResult) { } else if (!explainResult) {
content = <div />; content = <div />;
} else if ((explainResult as BasicQueryExplanation).query) { } else if (Array.isArray(explainResult) && explainResult.length) {
queryString = JSONBig.stringify( if (explainResult.length === 1) {
(explainResult as BasicQueryExplanation).query[0], content = renderQueryExplanation(explainResult[0]);
undefined, } else {
2, content = (
); <Tabs animate renderActiveTabPanelOnly vertical>
content = ( {explainResult.map((queryExplanation, i) => (
<div className="one-query"> <Tab
<FormGroup label="Query"> id={i}
<TextArea readOnly value={queryString} /> key={i}
</FormGroup> title={`Query ${i + 1}`}
{(explainResult as BasicQueryExplanation).signature && ( panel={renderQueryExplanation(queryExplanation)}
<FormGroup label="Signature">
<InputGroup
defaultValue={(explainResult as BasicQueryExplanation).signature || ''}
readOnly
/> />
</FormGroup> ))}
)} <Tabs.Expander />
</div> </Tabs>
); );
} 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>
);
} else { } else {
content = <div className="generic-result">{explainResult}</div>; 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}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} /> <Button text="Close" onClick={onClose} />
{queryString && (
<Button
text="Open query"
intent={Intent.PRIMARY}
onClick={() => {
if (queryString) setQueryString(queryString);
onClose();
}}
/>
)}
</div> </div>
</div> </div>
</Dialog> </Dialog>