Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web(data-explorer): support show execution plan #1558

Merged
merged 7 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
web(data-explorer): support show execution plan
  • Loading branch information
634750802 committed Jul 31, 2023
commit f7420d99b492c78c429f15baa2ea239be4e30a0d
9 changes: 9 additions & 0 deletions web/src/api/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface Question {
engines: string[];
queueJobId?: string | null;
result?: QuestionSQLResult;
plan?: QuestionSQLPlan[];
chart?: ChartResult | null;
recommended: boolean;
recommendedQuestions?: string[];
Expand Down Expand Up @@ -60,6 +61,14 @@ export interface QuestionSQLResult {
rows: Array<Record<string, any>>;
}

export interface QuestionSQLPlan {
'id': string;
'estRows': string;
'task': string;
'access object': string;
'operator info': string;
}

export interface QuestionQueryResult {
result: QuestionSQLResult;
executedAt: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Box, Dialog, Tab, Tabs, useEventCallback } from '@mui/material';
import { Question } from '@site/src/api/explorer';
import CodeBlock from '@theme/CodeBlock';
import React, { useMemo, useState } from 'react';
import { format } from 'sql-formatter';

export interface ExecutionInfoDialogProps {
question?: Question;
open: boolean;
onOpenChange?: (open: boolean) => void;
}

export default function ExecutionInfoDialog ({ question, open, onOpenChange }: ExecutionInfoDialogProps) {
const [type, setType] = useState<'sql' | 'plan'>('sql');

const handleClose = useEventCallback(() => {
onOpenChange?.(false);
});

const handleTypeChange = useEventCallback((ev: any, type: string) => {
setType(type === 'plan' ? 'plan' : 'sql');
});

const formattedSql = useMemo(() => {
try {
return format(question?.querySQL ?? '', { language: 'mysql' });
} catch {
return question?.querySQL ?? '';
}
}, [question?.querySQL]);

const renderChild = () => {
if (type === 'plan') {
const executionPlanKeys = ['id', 'estRows', 'task', 'access object', 'operator info'] as const;

return (
<Box sx={{ overflowX: 'scroll', color: 'rgb(248, 248, 242)', backgroundColor: 'rgb(40, 42, 54)', borderRadius: 2, py: 2 }} mb={2}>
<Box display="table" fontFamily="monospace" fontSize={16} lineHeight={1} sx={{ borderSpacing: '16px 0' }}>
<Box display="table-header-group">
<Box display="table-row">
{executionPlanKeys.map(field => (
<Box key={field} display="table-cell">{field}</Box>
))}
</Box>
</Box>
<Box display="table-footer-group">
{question?.plan?.map((item, i) => (
<Box key={i} display="table-row">
{executionPlanKeys.map(field => (
<Box key={field} display="table-cell" whiteSpace="pre">{item[field]}</Box>
))}
</Box>
))}
</Box>
</Box>
</Box>
);
} else {
return (
<CodeBlock className="language-sql">
{formattedSql}
</CodeBlock>
);
}
};

return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="xl"
fullWidth={true}
>
<Tabs onChange={handleTypeChange} value={type} sx={{ mb: 2 }}>
<Tab label="SQL" value="sql" />
<Tab label="Execution Plan" value="plan" />
</Tabs>
<Box px={2}>
{renderChild()}
</Box>
</Dialog>
);
}
52 changes: 34 additions & 18 deletions web/src/pages/explore/_components/ResultSection/ResultSection.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import Section, { SectionProps, SectionStatus, SectionStatusIcon } from '@site/src/pages/explore/_components/Section';
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { QuestionLoadingPhase } from '@site/src/pages/explore/_components/useQuestion';
import { isEmptyArray, isNonemptyString, isNullish, nonEmptyArray, notFalsy, notNullish } from '@site/src/utils/value';
import { ChartResult, Question, QuestionStatus } from '@site/src/api/explorer';
import Info from '@site/src/pages/explore/_components/Info';
import Link from '@docusaurus/Link';
import { ArrowRightAlt, AutoGraph, TableView } from '@mui/icons-material';
import { TabContext, TabPanel } from '@mui/lab';
import { Box, Divider, Portal, Stack, styled, ToggleButton, ToggleButtonGroup, Typography, useEventCallback } from '@mui/material';
import ErrorBlock from '@site/src/pages/explore/_components/ErrorBlock';
import TableChart from '@site/src/pages/explore/_components/charts/TableChart';
import { ChartResult, Question, QuestionStatus } from '@site/src/api/explorer';
import Anchor from '@site/src/components/Anchor';
import { Charts } from '@site/src/pages/explore/_components/charts';
import { AutoGraph, TableView } from '@mui/icons-material';
import { TabContext, TabPanel } from '@mui/lab';
import TableChart from '@site/src/pages/explore/_components/charts/TableChart';
import { useExploreContext } from '@site/src/pages/explore/_components/context';
import SummaryCard from '@site/src/pages/explore/_components/SummaryCard';
import { uniqueItems } from '@site/src/utils/generate';
import ShareButtons from '../ShareButtons';
import TypewriterEffect from '@site/src/pages/explore/_components/TypewriterEffect';
import ErrorBlock from '@site/src/pages/explore/_components/ErrorBlock';
import Feedback from '@site/src/pages/explore/_components/Feedback';
import Info from '@site/src/pages/explore/_components/Info';
import { Prompts } from '@site/src/pages/explore/_components/Prompt';
import Link from '@docusaurus/Link';
import Anchor from '@site/src/components/Anchor';
import ErrorMessage from '@site/src/pages/explore/_components/ResultSection/ErrorMessage';
import ShowExecutionInfoButton from '@site/src/pages/explore/_components/ResultSection/ShowExecutionInfoButton';
import Section, { SectionProps, SectionStatus, SectionStatusIcon } from '@site/src/pages/explore/_components/Section';
import SummaryCard from '@site/src/pages/explore/_components/SummaryCard';
import TypewriterEffect from '@site/src/pages/explore/_components/TypewriterEffect';
import { QuestionLoadingPhase } from '@site/src/pages/explore/_components/useQuestion';
import { uniqueItems } from '@site/src/utils/generate';
import { isEmptyArray, isNonemptyString, isNullish, nonEmptyArray, notFalsy, notNullish } from '@site/src/utils/value';
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { makeIssueTemplate } from '../issueTemplates';
import ShareButtons from '../ShareButtons';

const ENABLE_SUMMARY = false;

Expand Down Expand Up @@ -69,7 +70,12 @@ const ResultSection = forwardRef<HTMLElement, ResultSectionProps>(({ question, p
case QuestionLoadingPhase.VISUALIZE_FAILED:
case QuestionLoadingPhase.READY:
case QuestionLoadingPhase.SUMMARIZING:
return <>{`${question?.result?.rows.length ?? 'NaN'} rows in ${question?.spent ?? 'NaN'} seconds`}{renderEngines(question)}</>;
return (
<>
{`${question?.result?.rows.length ?? 'NaN'} rows in ${question?.spent ?? 'NaN'} seconds`}
{renderEngines(question)}
{renderExecutionPlan(question)}
</>);
default:
return 'pending';
}
Expand Down Expand Up @@ -169,7 +175,7 @@ function renderEngines (question: Question | undefined) {
<>
. Running on&nbsp;
<EngineTag>
<TidbLogo src='/img/tidb-cloud-logo-t.svg' alt='TiDB' />
<TidbLogo src="/img/tidb-cloud-logo-t.svg" alt="TiDB" />
{question.engines.map(replaceEngineName).join(', ')}
</EngineTag>
<Info>
Expand All @@ -185,6 +191,16 @@ function renderEngines (question: Question | undefined) {
return null;
}

function renderExecutionPlan (question: Question | undefined) {
if (notNullish(question) && !isEmptyArray(question.plan)) {
return (
<ShowExecutionInfoButton question={question}>
Show execution plan <ArrowRightAlt />
</ShowExecutionInfoButton>
);
}
}

function replaceEngineName (name: string) {
switch (name) {
case 'tiflash':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Button, useEventCallback } from '@mui/material';
import { Question } from '@site/src/api/explorer';
import ExecutionInfoDialog from '@site/src/pages/explore/_components/ResultSection/ExecutionInfoDialog';
import React, { ReactNode, useState } from 'react';

export default function ShowExecutionInfoButton ({ question, children }: { question: Question, children: ReactNode }) {
const [open, setOpen] = useState(false);

const handleOpen = useEventCallback(() => {
setOpen(true);
});

return (
<>
<Button variant="text" size="small" sx={{ ml: 1 }} onClick={handleOpen}>
{children}
</Button>
<ExecutionInfoDialog open={open} onOpenChange={setOpen} question={question} />
</>
);
}