Skip to content

Commit fb007a7

Browse files
astandrikAnton Standrik
andauthored
feat: add to embedded ui link to plan2svg converter (#1619)
Co-authored-by: Anton Standrik <astandrik@Antons-MacBook-Air.local>
1 parent 9972ac2 commit fb007a7

File tree

9 files changed

+142
-3
lines changed

9 files changed

+142
-3
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import type {ValueOf} from '../../../../types/common';
1818
import type {ExecuteQueryResult} from '../../../../types/store/executeQuery';
1919
import {getArray} from '../../../../utils';
2020
import {cn} from '../../../../utils/cn';
21+
import {USE_SHOW_PLAN_SVG_KEY} from '../../../../utils/constants';
2122
import {getStringifiedData} from '../../../../utils/dataFormatters/dataFormatters';
22-
import {useTypedDispatch} from '../../../../utils/hooks';
23+
import {useSetting, useTypedDispatch} from '../../../../utils/hooks';
2324
import {parseQueryError} from '../../../../utils/query';
2425
import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers';
2526
import {CancelQueryButton} from '../CancelQueryButton/CancelQueryButton';
@@ -30,6 +31,7 @@ import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner';
3031
import {getPreparedResult} from '../utils/getPreparedResult';
3132
import {isQueryCancelledError} from '../utils/isQueryCancelledError';
3233

34+
import {PlanToSvgButton} from './PlanToSvgButton';
3335
import {TraceButton} from './TraceButton';
3436
import i18n from './i18n';
3537
import {getPlan} from './utils';
@@ -67,6 +69,7 @@ export function ExecuteResult({
6769
const [selectedResultSet, setSelectedResultSet] = React.useState(0);
6870
const [activeSection, setActiveSection] = React.useState<SectionID>(resultOptionsIds.result);
6971
const dispatch = useTypedDispatch();
72+
const [useShowPlanToSvg] = useSetting<boolean>(USE_SHOW_PLAN_SVG_KEY);
7073

7174
const {error, isLoading, queryId, data} = result;
7275

@@ -273,6 +276,9 @@ export function ExecuteResult({
273276
{data?.traceId ? (
274277
<TraceButton traceId={data.traceId} isTraceReady={result.isTraceReady} />
275278
) : null}
279+
{data?.plan && useShowPlanToSvg ? (
280+
<PlanToSvgButton plan={data?.plan} database={tenantName} />
281+
) : null}
276282
</div>
277283
<div className={b('controls-left')}>
278284
{renderClipboardButton()}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
3+
import {ArrowUpRightFromSquare} from '@gravity-ui/icons';
4+
import {Button, Tooltip} from '@gravity-ui/uikit';
5+
6+
import {planToSvgApi} from '../../../../store/reducers/planToSvg';
7+
import type {QueryPlan, ScriptPlan} from '../../../../types/api/query';
8+
9+
import i18n from './i18n';
10+
11+
function getButtonView(error: string | null, isLoading: boolean) {
12+
if (error) {
13+
return 'flat-danger';
14+
}
15+
return isLoading ? 'flat-secondary' : 'flat-info';
16+
}
17+
18+
interface PlanToSvgButtonProps {
19+
plan: QueryPlan | ScriptPlan;
20+
database: string;
21+
}
22+
23+
export function PlanToSvgButton({plan, database}: PlanToSvgButtonProps) {
24+
const [error, setError] = React.useState<string | null>(null);
25+
const [blobUrl, setBlobUrl] = React.useState<string | null>(null);
26+
const [getPlanToSvg, {isLoading}] = planToSvgApi.usePlanToSvgQueryMutation();
27+
28+
const handleClick = React.useCallback(() => {
29+
getPlanToSvg({plan, database})
30+
.unwrap()
31+
.then((result) => {
32+
const blob = new Blob([result], {type: 'image/svg+xml'});
33+
const url = URL.createObjectURL(blob);
34+
setBlobUrl(url);
35+
setError(null);
36+
window.open(url, '_blank');
37+
})
38+
.catch((err) => {
39+
setError(JSON.stringify(err));
40+
});
41+
}, [database, getPlanToSvg, plan]);
42+
43+
React.useEffect(() => {
44+
return () => {
45+
if (blobUrl) {
46+
URL.revokeObjectURL(blobUrl);
47+
}
48+
};
49+
}, [blobUrl]);
50+
51+
return (
52+
<Tooltip
53+
content={error ? i18n('text_error-plan-svg', {error}) : i18n('text_open-plan-svg')}
54+
>
55+
<Button
56+
view={getButtonView(error, isLoading)}
57+
loading={isLoading}
58+
onClick={handleClick}
59+
disabled={isLoading}
60+
>
61+
{i18n('text_plan-svg')}
62+
<Button.Icon>
63+
<ArrowUpRightFromSquare />
64+
</Button.Icon>
65+
</Button>
66+
</Tooltip>
67+
);
68+
}

src/containers/Tenant/Query/ExecuteResult/i18n/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
"action.copy": "Copy {{activeSection}}",
88
"trace": "Trace",
99
"title.truncated": "Truncated",
10-
"title.result": "Result"
10+
"title.result": "Result",
11+
"text_plan-svg": "Execution plan",
12+
"text_open-plan-svg": "Open execution plan in new window",
13+
"text_error-plan-svg": "Error: {{error}}"
1114
}

src/containers/UserSettings/i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"settings.usePaginatedTables.title": "Use paginated tables",
3434
"settings.usePaginatedTables.description": " Use table with data load on scroll for Nodes and Storage tabs. It will increase performance, but could work unstable",
3535

36+
"settings.useShowPlanToSvg.title": "Plan to svg",
37+
"settings.useShowPlanToSvg.description": " Show \"Plan to svg\" button in query result widow (if query was executed with full stats option).",
38+
3639
"settings.showDomainDatabase.title": "Show domain database",
3740

3841
"settings.useClusterBalancerAsBackend.title": "Use cluster balancer as backend",

src/containers/UserSettings/settings.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
THEME_KEY,
1313
USE_CLUSTER_BALANCER_AS_BACKEND_KEY,
1414
USE_PAGINATED_TABLES_KEY,
15+
USE_SHOW_PLAN_SVG_KEY,
1516
} from '../../utils/constants';
1617
import {Lang, defaultLang} from '../../utils/i18n';
1718

@@ -96,6 +97,12 @@ export const usePaginatedTables: SettingProps = {
9697
description: i18n('settings.usePaginatedTables.description'),
9798
};
9899

100+
export const useShowPlanToSvgTables: SettingProps = {
101+
settingKey: USE_SHOW_PLAN_SVG_KEY,
102+
title: i18n('settings.useShowPlanToSvg.title'),
103+
description: i18n('settings.useShowPlanToSvg.description'),
104+
};
105+
99106
export const showDomainDatabase: SettingProps = {
100107
settingKey: SHOW_DOMAIN_DATABASE_KEY,
101108
title: i18n('settings.showDomainDatabase.title'),
@@ -138,7 +145,7 @@ export const appearanceSection: SettingsSection = {
138145
export const experimentsSection: SettingsSection = {
139146
id: 'experimentsSection',
140147
title: i18n('section.experiments'),
141-
settings: [usePaginatedTables],
148+
settings: [usePaginatedTables, useShowPlanToSvgTables],
142149
};
143150
export const devSettingsSection: SettingsSection = {
144151
id: 'devSettingsSection',

src/services/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {AxiosRequestConfig} from 'axios';
44
import axiosRetry from 'axios-retry';
55

66
import {backend as BACKEND, metaBackend as META_BACKEND} from '../store';
7+
import type {PlanToSvgQueryParams} from '../store/reducers/planToSvg';
78
import type {TMetaInfo} from '../types/api/acl';
89
import type {TQueryAutocomplete} from '../types/api/autocomplete';
910
import type {CapabilitiesResponse} from '../types/api/capabilities';
@@ -578,6 +579,22 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
578579
},
579580
);
580581
}
582+
planToSvg({database, plan}: PlanToSvgQueryParams, {signal}: {signal?: AbortSignal} = {}) {
583+
return this.post<string>(
584+
this.getPath('/viewer/plan2svg'),
585+
plan,
586+
{database},
587+
{
588+
requestConfig: {
589+
signal,
590+
responseType: 'text',
591+
headers: {
592+
Accept: 'image/svg+xml',
593+
},
594+
},
595+
},
596+
);
597+
}
581598
getHotKeys(
582599
{path, database, enableSampling}: {path: string; database: string; enableSampling: boolean},
583600
{concurrentId, signal}: AxiosOptions = {},

src/services/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
THEME_KEY,
2020
USE_CLUSTER_BALANCER_AS_BACKEND_KEY,
2121
USE_PAGINATED_TABLES_KEY,
22+
USE_SHOW_PLAN_SVG_KEY,
2223
} from '../utils/constants';
2324
import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../utils/query';
2425
import {parseJson} from '../utils/utils';
@@ -37,6 +38,7 @@ export const DEFAULT_USER_SETTINGS = {
3738
[ASIDE_HEADER_COMPACT_KEY]: true,
3839
[PARTITIONS_HIDDEN_COLUMNS_KEY]: [],
3940
[USE_PAGINATED_TABLES_KEY]: true,
41+
[USE_SHOW_PLAN_SVG_KEY]: false,
4042
[USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true,
4143
[ENABLE_AUTOCOMPLETE]: true,
4244
[AUTOCOMPLETE_ON_ENTER]: true,

src/store/reducers/planToSvg.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type {QueryPlan, ScriptPlan} from '../../types/api/query';
2+
3+
import {api} from './api';
4+
5+
export interface PlanToSvgQueryParams {
6+
plan: ScriptPlan | QueryPlan;
7+
database: string;
8+
}
9+
10+
export const planToSvgApi = api.injectEndpoints({
11+
endpoints: (build) => ({
12+
planToSvgQuery: build.mutation<string, PlanToSvgQueryParams>({
13+
queryFn: async ({plan, database}, {signal}) => {
14+
try {
15+
const response = await window.api.planToSvg(
16+
{
17+
database,
18+
plan,
19+
},
20+
{signal},
21+
);
22+
23+
return {data: response};
24+
} catch (error) {
25+
return {error};
26+
}
27+
},
28+
}),
29+
}),
30+
overrideExisting: 'throw',
31+
});

src/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export const TENANT_INITIAL_PAGE_KEY = 'saved_tenant_initial_tab';
134134
// Old key value for backward compatibility
135135
export const USE_PAGINATED_TABLES_KEY = 'useBackendParamsForTables';
136136

137+
export const USE_SHOW_PLAN_SVG_KEY = 'useShowPlanToSvg';
138+
137139
// Setting to hide domain in database list
138140
export const SHOW_DOMAIN_DATABASE_KEY = 'showDomainDatabase';
139141

0 commit comments

Comments
 (0)