Skip to content

Commit eb6c746

Browse files
authored
feat(ExecuteResult): show query schema with stats (#1083)
1 parent e92c571 commit eb6c746

File tree

6 files changed

+170
-13
lines changed

6 files changed

+170
-13
lines changed

src/components/Graph/Graph.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
3+
import {getTopology, getYdbPlanNodeShape} from '@gravity-ui/paranoid';
4+
import type {Data, GraphNode, Options, Shapes} from '@gravity-ui/paranoid';
5+
6+
interface GraphProps<T> {
7+
data: Data<T>;
8+
opts?: Options;
9+
shapes?: Shapes;
10+
}
11+
12+
export function Graph<T>(props: GraphProps<T>) {
13+
const containerRef = React.useRef<HTMLDivElement>(null);
14+
const containerId = React.useId();
15+
16+
const {data, opts, shapes} = props;
17+
18+
React.useEffect(() => {
19+
const graphRoot = containerRef.current;
20+
if (!graphRoot) {
21+
return undefined;
22+
}
23+
24+
graphRoot.innerHTML = '';
25+
26+
const topology = getTopology(graphRoot.id, data, opts, shapes);
27+
topology.render();
28+
return () => {
29+
topology.destroy();
30+
};
31+
}, [data, opts, shapes]);
32+
33+
return <div id={containerId} ref={containerRef} style={{height: '100vh'}} />;
34+
}
35+
36+
export const renderExplainNode = (node: GraphNode): string => {
37+
const parts = node.name.split('|');
38+
return parts.length > 1 ? parts[1] : node.name;
39+
};
40+
41+
const schemaOptions: Options = {
42+
renderNodeTitle: renderExplainNode,
43+
textOverflow: 'normal' as const,
44+
initialZoomFitsCanvas: true,
45+
};
46+
47+
const schemaShapes = {
48+
node: getYdbPlanNodeShape,
49+
};
50+
51+
interface YDBGraphProps<T> {
52+
data: Data<T>;
53+
}
54+
55+
export function YDBGraph<T>(props: YDBGraphProps<T>) {
56+
return <Graph<T> {...props} opts={schemaOptions} shapes={schemaShapes} />;
57+
}

src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss

+7
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,11 @@
6767
padding: 10px;
6868
}
6969
}
70+
71+
&__explain-canvas-container {
72+
overflow-y: auto;
73+
74+
width: 100%;
75+
height: 100%;
76+
}
7077
}

src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx

+42-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from 'react';
22

3+
import type {ControlGroupOption} from '@gravity-ui/uikit';
34
import {RadioButton, Tabs} from '@gravity-ui/uikit';
45
import JSONTree from 'react-json-inspector';
56

67
import {ClipboardButton} from '../../../../components/ClipboardButton';
78
import Divider from '../../../../components/Divider/Divider';
89
import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton';
910
import Fullscreen from '../../../../components/Fullscreen/Fullscreen';
11+
import {YDBGraph} from '../../../../components/Graph/Graph';
1012
import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus';
1113
import {QueryResultTable} from '../../../../components/QueryResultTable/QueryResultTable';
1214
import {disableFullscreen} from '../../../../store/reducers/fullscreen';
@@ -22,51 +24,59 @@ import {ResultIssues} from '../Issues/Issues';
2224
import {QueryDuration} from '../QueryDuration/QueryDuration';
2325
import {getPreparedResult} from '../utils/getPreparedResult';
2426

27+
import {getPlan} from './utils';
28+
2529
import './ExecuteResult.scss';
2630

2731
const b = cn('ydb-query-execute-result');
2832

2933
const resultOptionsIds = {
3034
result: 'result',
3135
stats: 'stats',
36+
schema: 'schema',
3237
} as const;
3338

3439
type SectionID = ValueOf<typeof resultOptionsIds>;
3540

36-
const resultOptions = [
37-
{value: resultOptionsIds.result, content: 'Result'},
38-
{value: resultOptionsIds.stats, content: 'Stats'},
39-
];
40-
4141
interface ExecuteResultProps {
4242
data: IQueryResult | undefined;
43-
stats: IQueryResult['stats'] | undefined;
4443
error: unknown;
4544
isResultsCollapsed?: boolean;
4645
onCollapseResults: VoidFunction;
4746
onExpandResults: VoidFunction;
47+
theme?: string;
4848
}
4949

5050
export function ExecuteResult({
5151
data,
52-
stats,
5352
error,
5453
isResultsCollapsed,
5554
onCollapseResults,
5655
onExpandResults,
56+
theme,
5757
}: ExecuteResultProps) {
5858
const [selectedResultSet, setSelectedResultSet] = React.useState(0);
5959
const [activeSection, setActiveSection] = React.useState<SectionID>(resultOptionsIds.result);
6060

6161
const isFullscreen = useTypedSelector((state) => state.fullscreen);
6262
const dispatch = useTypedDispatch();
6363

64+
const stats = data?.stats;
6465
const resultsSetsCount = data?.resultSets?.length;
6566
const isMulti = resultsSetsCount && resultsSetsCount > 0;
6667
const currentResult = isMulti ? data?.resultSets?.[selectedResultSet].result : data?.result;
6768
const currentColumns = isMulti ? data?.resultSets?.[selectedResultSet].columns : data?.columns;
6869
const textResults = getPreparedResult(currentResult);
6970
const copyDisabled = !textResults.length;
71+
const plan = React.useMemo(() => getPlan(data), [data]);
72+
73+
const resultOptions: ControlGroupOption<SectionID>[] = [
74+
{value: resultOptionsIds.result, content: 'Result'},
75+
{value: resultOptionsIds.stats, content: 'Stats'},
76+
];
77+
if (plan) {
78+
resultOptions.push({value: resultOptionsIds.schema, content: 'Schema'});
79+
}
7080

7181
const parsedError = parseQueryError(error);
7282

@@ -145,6 +155,27 @@ export function ExecuteResult({
145155
);
146156
};
147157

158+
const renderSchema = () => {
159+
const isEnoughDataForGraph = plan?.links && plan?.nodes && plan?.nodes.length;
160+
161+
const content = isEnoughDataForGraph ? (
162+
<div className={b('explain-canvas-container')}>
163+
<YDBGraph key={theme} data={plan} />
164+
</div>
165+
) : null;
166+
167+
return (
168+
<React.Fragment>
169+
{!isFullscreen && content}
170+
{isFullscreen && (
171+
<Fullscreen>
172+
<div className={b('result-fullscreen-wrapper')}>{content}</div>
173+
</Fullscreen>
174+
)}
175+
</React.Fragment>
176+
);
177+
};
178+
148179
const renderResult = () => {
149180
const content = renderContent();
150181

@@ -190,6 +221,10 @@ export function ExecuteResult({
190221
return renderResult();
191222
}
192223

224+
if (activeSection === resultOptionsIds.schema && !error) {
225+
return renderSchema();
226+
}
227+
193228
return (
194229
<div className={b('result')}>
195230
{activeSection === resultOptionsIds.stats && !error && renderStats()}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {explainVersions} from '../../../../store/reducers/explainQuery/utils';
2+
import type {IQueryResult} from '../../../../types/store/query';
3+
import {preparePlan} from '../../../../utils/prepareQueryExplain';
4+
import {parseQueryExplainPlan} from '../../../../utils/query';
5+
6+
export function getPlan(data: IQueryResult | undefined) {
7+
if (!data) {
8+
return undefined;
9+
}
10+
11+
const {plan} = data;
12+
13+
if (plan) {
14+
const queryPlan = parseQueryExplainPlan(plan);
15+
const isSupportedVersion = queryPlan.meta.version === explainVersions.v2;
16+
if (!isSupportedVersion) {
17+
return undefined;
18+
}
19+
20+
const planWithStats = queryPlan.Plan;
21+
if (!planWithStats) {
22+
return undefined;
23+
}
24+
return {
25+
...preparePlan(planWithStats),
26+
tables: queryPlan.tables,
27+
};
28+
}
29+
30+
const {stats} = data;
31+
const planFromStats = stats?.Executions?.[0]?.TxPlansWithStats?.[0];
32+
if (!planFromStats) {
33+
return undefined;
34+
}
35+
try {
36+
const planWithStats = JSON.parse(planFromStats);
37+
return preparePlan(planWithStats);
38+
} catch (e) {
39+
return undefined;
40+
}
41+
}

src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -474,16 +474,14 @@ function Result({
474474

475475
if (resultType === RESULT_TYPES.EXECUTE) {
476476
if (executeQueryData || executeQueryError) {
477-
const {stats, ...data} = executeQueryData || {};
478-
479477
return (
480478
<ExecuteResult
481-
data={data}
482-
stats={stats}
479+
data={executeQueryData}
483480
error={executeQueryError}
484481
isResultsCollapsed={resultVisibilityState.collapsed}
485482
onExpandResults={onExpandResultHandler}
486483
onCollapseResults={onCollapseResultHandler}
484+
theme={theme}
487485
/>
488486
);
489487
}

src/utils/prepareQueryExplain.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function prepareStats(plan: PlanNode) {
2525
continue;
2626
}
2727

28-
const value = Array.isArray(data) ? data.join(', ') : data;
28+
const value = typeof data === 'string' ? data : JSON.stringify(data);
2929
section.items.push({name, value});
3030
}
3131

@@ -46,7 +46,10 @@ function prepareStats(plan: PlanNode) {
4646
continue;
4747
}
4848

49-
attrStats.push({name: key, value: String(value)});
49+
attrStats.push({
50+
name: key,
51+
value: typeof value === 'string' ? value : JSON.stringify(value),
52+
});
5053
}
5154

5255
if (attrStats.length > 0) {
@@ -57,6 +60,22 @@ function prepareStats(plan: PlanNode) {
5760
}
5861
}
5962

63+
if (plan.Stats) {
64+
const attrStats: TopologyNodeDataStatsItem[] = [];
65+
66+
for (const [key, value] of Object.entries(plan.Stats)) {
67+
attrStats.push({
68+
name: key,
69+
value: typeof value === 'string' ? value : JSON.stringify(value),
70+
});
71+
}
72+
73+
stats.push({
74+
group: 'Stats',
75+
stats: attrStats,
76+
});
77+
}
78+
6079
return stats;
6180
}
6281

0 commit comments

Comments
 (0)