diff --git a/web/src/api/explorer.ts b/web/src/api/explorer.ts index d6479259563..0916ac77206 100644 --- a/web/src/api/explorer.ts +++ b/web/src/api/explorer.ts @@ -18,6 +18,7 @@ export interface Question { engines: string[]; queueJobId?: string | null; result?: QuestionSQLResult; + plan?: QuestionSQLPlan[]; chart?: ChartResult | null; recommended: boolean; recommendedQuestions?: string[]; @@ -60,6 +61,14 @@ export interface QuestionSQLResult { rows: Array>; } +export interface QuestionSQLPlan { + 'id': string; + 'estRows': string; + 'task': string; + 'access object': string; + 'operator info': string; +} + export interface QuestionQueryResult { result: QuestionSQLResult; executedAt: string; diff --git a/web/src/pages/explore/_components/ResultSection/ErrorMessage.tsx b/web/src/pages/explore/_components/ResultSection/ErrorMessage.tsx index 8b777e0426f..fa2d2c53dd5 100644 --- a/web/src/pages/explore/_components/ResultSection/ErrorMessage.tsx +++ b/web/src/pages/explore/_components/ResultSection/ErrorMessage.tsx @@ -7,7 +7,7 @@ import Link from '@docusaurus/Link'; export default function ErrorMessage () { const { question } = useQuestionManagement(); - if (question?.status !== 'error') { + if (question?.status !== 'error' && question?.status !== 'cancel') { return <>; } diff --git a/web/src/pages/explore/_components/ResultSection/ExecutionInfoDialog.tsx b/web/src/pages/explore/_components/ResultSection/ExecutionInfoDialog.tsx new file mode 100644 index 00000000000..05a6f74d8ba --- /dev/null +++ b/web/src/pages/explore/_components/ResultSection/ExecutionInfoDialog.tsx @@ -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'>('plan'); + + 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 ( + + + + + {executionPlanKeys.map(field => ( + {field} + ))} + + + + {question?.plan?.map((item, i) => ( + + {executionPlanKeys.map(field => ( + {item[field]} + ))} + + ))} + + + + ); + } else { + return ( + + {formattedSql} + + ); + } + }; + + return ( + + + + + + + {renderChild()} + + + ); +} diff --git a/web/src/pages/explore/_components/ResultSection/ResultSection.tsx b/web/src/pages/explore/_components/ResultSection/ResultSection.tsx index c3f259bc1bc..7f1a30d0d18 100644 --- a/web/src/pages/explore/_components/ResultSection/ResultSection.tsx +++ b/web/src/pages/explore/_components/ResultSection/ResultSection.tsx @@ -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; @@ -63,13 +64,18 @@ const ResultSection = forwardRef(({ question, p case QuestionLoadingPhase.EXECUTING: return 'Running SQL...'; case QuestionLoadingPhase.EXECUTE_FAILED: - return 'Failed to execute SQL'; + return question?.status === 'cancel' ? 'Execution canceled' : 'Failed to execute SQL'; case QuestionLoadingPhase.UNKNOWN_ERROR: return 'Unknown error'; 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'; } @@ -169,7 +175,7 @@ function renderEngines (question: Question | undefined) { <> . Running on  - + {question.engines.map(replaceEngineName).join(', ')} @@ -185,6 +191,16 @@ function renderEngines (question: Question | undefined) { return null; } +function renderExecutionPlan (question: Question | undefined) { + if (notNullish(question) && !isEmptyArray(question.plan)) { + return ( + + Explain SQL + + ); + } +} + function replaceEngineName (name: string) { switch (name) { case 'tiflash': diff --git a/web/src/pages/explore/_components/ResultSection/ShowExecutionInfoButton.tsx b/web/src/pages/explore/_components/ResultSection/ShowExecutionInfoButton.tsx new file mode 100644 index 00000000000..05cd06a5a2b --- /dev/null +++ b/web/src/pages/explore/_components/ResultSection/ShowExecutionInfoButton.tsx @@ -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 ( + <> + + + + ); +} diff --git a/web/src/pages/explore/_components/SqlSection/AIMessagesV3.tsx b/web/src/pages/explore/_components/SqlSection/AIMessagesV3.tsx index 0c949f4b4e2..9a5cf237123 100644 --- a/web/src/pages/explore/_components/SqlSection/AIMessagesV3.tsx +++ b/web/src/pages/explore/_components/SqlSection/AIMessagesV3.tsx @@ -11,6 +11,23 @@ import { ExpandMore } from '@mui/icons-material'; import { useSafeSetTimeout } from '@site/src/hooks/mounted'; import BotIcon from '@site/src/pages/explore/_components/BotIcon'; +function getTime (name: string, fallback: number): number { + if (typeof localStorage === 'undefined') { + return fallback; + } + const i = localStorage.getItem(`ossinsight.explore.ai-message.${name}`); + if (i) { + return parseInt(i); + } else { + return fallback; + } +} + +const SUB_DELAY = getTime('sub-delay', 1000); +const PACE_NORMAL = getTime('pace-normal', 25); +const PACE_SPACE = getTime('pace-space', 40); +const PACE_RANDOM_RANGE = getTime('pace-random-range', 25); + export interface AIMessagesV3Props extends AIMessagesV2Props { } @@ -212,7 +229,7 @@ class QuestionModel { private renderMessage () { return ( - this.next()} timeout={1000}> + this.next()} timeout={SUB_DELAY}> You can copy and revise it based on the question above 👆. @@ -372,9 +389,9 @@ export function defaultGetPace ( ): number { switch (lastChar) { case ' ': - return 40 + Math.random() * 25; + return PACE_SPACE + Math.random() * PACE_RANDOM_RANGE; default: - return 25 + Math.random() * 25; + return PACE_NORMAL + Math.random() * PACE_RANDOM_RANGE; } } diff --git a/web/src/pages/home/_sections/2-toplistv2/hook.tsx b/web/src/pages/home/_sections/2-toplistv2/hook.tsx index 19b911dd790..2479c22701c 100644 --- a/web/src/pages/home/_sections/2-toplistv2/hook.tsx +++ b/web/src/pages/home/_sections/2-toplistv2/hook.tsx @@ -1,4 +1,4 @@ -import { params } from '@query/trending-repos/params.json'; +import { params as _params } from '@query/trending-repos/params.json'; import { AsyncData, RemoteData, useRemoteData } from '@site/src/components/RemoteCharts/hook'; import React, { DependencyList, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelectParam } from '@site/src/components/params'; @@ -10,8 +10,10 @@ import { isNullish } from '@site/src/utils/value'; export type Language = string; export type Period = string; +const params = _params as any; + export const periods: Period[] = params.find(param => param.name === 'period')?.enums ?? []; -export const languages: Language[] = Object.keys(params.find(param => param.name === 'language')?.template ?? {}); +export const languages: Language[] = params.find(param => param.name === 'language')?.enums ?? []; const periodOptions = periods.map(period => ({ key: period, title: snakeToCamel(period) })); const languageOptions = languages.map(language => ({ key: language, label: language }));