import React from 'react'; import type {Settings} from '@gravity-ui/react-data-table'; import type {ControlGroupOption, CopyToClipboardStatus} from '@gravity-ui/uikit'; import { ActionTooltip, Button, ClipboardIcon, Flex, SegmentedRadioGroup, Text, } from '@gravity-ui/uikit'; import {EnableFullscreenButton} from '../../../../components/EnableFullscreenButton/EnableFullscreenButton'; import {Fullscreen} from '../../../../components/Fullscreen/Fullscreen'; import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import {selectResultTab, setResultTab} from '../../../../store/reducers/query/query'; import type {QueryResult} from '../../../../store/reducers/query/types'; import type {QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {getIllustration} from '../../../../utils/illustrations'; import {isExecutionQueryAction} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; import {QueryStoppedBanner} from '../QueryStoppedBanner/QueryStoppedBanner'; import {copyResultToClipboard, copyTextDataToClipboard} from '../utils/copyToClipboard'; import {isQueryCancelledError} from '../utils/isQueryCancelledError'; import {PlanSection} from './components/PlanSection/PlanSection'; import {QueryInfoDropdown} from './components/QueryInfoDropdown/QueryInfoDropdown'; import {QueryResultError} from './components/QueryResultError/QueryResultError'; import {ResultSetsViewer} from './components/ResultSetsViewer/ResultSetsViewer'; import {TraceButton} from './components/TraceButton/TraceButton'; import type {SectionID} from './constants'; import {RESULT_OPTIONS_IDS, getDefaultResultSection, getResultSections} from './constants'; import i18n from './i18n'; import {canShowPlanToSvg} from './utils'; import './QueryResultViewer.scss'; const b = cn('ydb-query-result'); const COPY_STATUS_RESET_TIMEOUT = 1500; const RESULT_OPTIONS_TITLES: Record = { get result() { return i18n('action.result'); }, get schema() { return i18n('action.schema'); }, get simplified() { return i18n('action.explain-plan'); }, get json() { return i18n('action.json'); }, get stats() { return i18n('action.stats'); }, get ast() { return i18n('action.ast'); }, }; interface ExecuteResultProps { result: QueryResult; resultType?: QueryAction; isResultsCollapsed?: boolean; theme?: string; database: string; queryText?: string; tableSettings?: Partial; onCollapseResults: VoidFunction; onExpandResults: VoidFunction; } export function QueryResultViewer({ result, resultType = 'execute', isResultsCollapsed, theme, database, queryText, tableSettings, onCollapseResults, onExpandResults, }: ExecuteResultProps) { const InternalErrorImage = getIllustration('InternalError'); const dispatch = useTypedDispatch(); const selectedTabs = useTypedSelector(selectResultTab); const isExecutionResult = isExecutionQueryAction(resultType); const hasResultSection = getResultSections(resultType).includes(RESULT_OPTIONS_IDS.result); const [selectedResultSet, setSelectedResultSet] = React.useState(0); const [copyStatus, setCopyStatus] = React.useState('pending'); const copyTimeoutRef = React.useRef(); const activeSection: SectionID = React.useMemo(() => { const savedTab = selectedTabs?.[resultType]; if (savedTab) { const validSections = getResultSections(resultType); if (validSections.includes(savedTab as SectionID)) { return savedTab as SectionID; } } return getDefaultResultSection(resultType); }, [selectedTabs, resultType]); const {error, isLoading, streamingStatus, data = {}} = result; const {preparedPlan, simplifiedPlan, stats, resultSets, ast} = data; React.useEffect(() => { return () => { dispatch(disableFullscreen()); window.clearTimeout(copyTimeoutRef.current); }; }, [dispatch]); const onSelectSection = (value: SectionID) => { dispatch(setResultTab({queryType: resultType, tabId: value})); setCopyStatus('pending'); window.clearTimeout(copyTimeoutRef.current); }; const radioButtonOptions: ControlGroupOption[] = React.useMemo(() => { const sections = getResultSections(resultType); return sections.map((section) => ({ value: section, content: RESULT_OPTIONS_TITLES[section], })); }, [resultType]); const hasCopyableData = React.useCallback((): boolean => { switch (activeSection) { case RESULT_OPTIONS_IDS.result: return Boolean(data?.resultSets?.[selectedResultSet]?.result?.length); case RESULT_OPTIONS_IDS.json: return Boolean(preparedPlan?.pristine); case RESULT_OPTIONS_IDS.simplified: return Boolean(simplifiedPlan?.pristine); case RESULT_OPTIONS_IDS.stats: return Boolean(stats); case RESULT_OPTIONS_IDS.ast: return Boolean(ast); default: return false; } }, [ activeSection, data?.resultSets, selectedResultSet, preparedPlan?.pristine, simplifiedPlan?.pristine, stats, ast, ]); const handleCopy = React.useCallback(async () => { window.clearTimeout(copyTimeoutRef.current); let success = false; switch (activeSection) { case RESULT_OPTIONS_IDS.result: { const currentResult = data?.resultSets?.[selectedResultSet]; success = await copyResultToClipboard(currentResult?.result); break; } case RESULT_OPTIONS_IDS.json: success = await copyTextDataToClipboard(preparedPlan?.pristine); break; case RESULT_OPTIONS_IDS.simplified: success = await copyTextDataToClipboard(simplifiedPlan?.pristine); break; case RESULT_OPTIONS_IDS.stats: success = await copyTextDataToClipboard(stats); break; case RESULT_OPTIONS_IDS.ast: success = await copyTextDataToClipboard(ast); break; } setCopyStatus(success ? 'success' : 'error'); copyTimeoutRef.current = window.setTimeout(() => { setCopyStatus('pending'); }, COPY_STATUS_RESET_TIMEOUT); }, [ activeSection, data?.resultSets, selectedResultSet, preparedPlan?.pristine, simplifiedPlan?.pristine, stats, ast, ]); const getCopyTooltipTitle = (status: CopyToClipboardStatus): string => { if (status === 'success') { return i18n('action.copy-success'); } if (status === 'error') { return i18n('action.copy-error'); } return i18n('action.copy', {activeSection}); }; const renderCopyButton = () => { if (isLoading || !hasCopyableData()) { return null; } return ( ); }; const renderQueryInfoDropdown = () => { if (isLoading || isQueryCancelledError(error)) { return null; } return ( ); }; const renderCommonErrorView = (isStopped: boolean) => { return ( {isStopped ? i18n('stopped.title') : i18n('error.title')} {isStopped ? i18n('stopped.description') : i18n('error.description')} ); }; const renderResultSection = () => { const isStopped = isQueryCancelledError(error); if (activeSection === RESULT_OPTIONS_IDS.result) { if (error && isStopped && !resultSets?.length) { return renderCommonErrorView(isStopped); } return ( ); } if (error) { if ((isExecutionResult && hasResultSection) || isStopped) { return renderCommonErrorView(isStopped); } return ; } return ( ); }; const renderLeftControls = () => { return (
{radioButtonOptions.length && activeSection ? ( ) : null} {data?.traceId && isExecutionResult ? : null}
); }; const renderRightControls = () => { return (
{renderQueryInfoDropdown()} {renderCopyButton()}
); }; const isCancelled = isQueryCancelledError(error); return (
{renderLeftControls()} {renderRightControls()}
{isLoading || isCancelled ? null : } {isCancelled && data.resultSets?.length ? : null} {renderResultSection()}
); }