diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index 87326f0a02..a4a87a6393 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -18,8 +18,9 @@ import type {ValueOf} from '../../../../types/common'; import type {ExecuteQueryResult} from '../../../../types/store/executeQuery'; import {getArray} from '../../../../utils'; import {cn} from '../../../../utils/cn'; +import {USE_SHOW_PLAN_SVG_KEY} from '../../../../utils/constants'; import {getStringifiedData} from '../../../../utils/dataFormatters/dataFormatters'; -import {useTypedDispatch} from '../../../../utils/hooks'; +import {useSetting, useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryError} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {CancelQueryButton} from '../CancelQueryButton/CancelQueryButton'; @@ -30,6 +31,7 @@ import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; import {getPreparedResult} from '../utils/getPreparedResult'; import {isQueryCancelledError} from '../utils/isQueryCancelledError'; +import {PlanToSvgButton} from './PlanToSvgButton'; import {TraceButton} from './TraceButton'; import i18n from './i18n'; import {getPlan} from './utils'; @@ -67,6 +69,7 @@ export function ExecuteResult({ const [selectedResultSet, setSelectedResultSet] = React.useState(0); const [activeSection, setActiveSection] = React.useState(resultOptionsIds.result); const dispatch = useTypedDispatch(); + const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); const {error, isLoading, queryId, data} = result; @@ -273,6 +276,9 @@ export function ExecuteResult({ {data?.traceId ? ( ) : null} + {data?.plan && useShowPlanToSvg ? ( + + ) : null}
{renderClipboardButton()} diff --git a/src/containers/Tenant/Query/ExecuteResult/PlanToSvgButton.tsx b/src/containers/Tenant/Query/ExecuteResult/PlanToSvgButton.tsx new file mode 100644 index 0000000000..ae7ac15de4 --- /dev/null +++ b/src/containers/Tenant/Query/ExecuteResult/PlanToSvgButton.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import {ArrowUpRightFromSquare} from '@gravity-ui/icons'; +import {Button, Tooltip} from '@gravity-ui/uikit'; + +import {planToSvgApi} from '../../../../store/reducers/planToSvg'; +import type {QueryPlan, ScriptPlan} from '../../../../types/api/query'; + +import i18n from './i18n'; + +function getButtonView(error: string | null, isLoading: boolean) { + if (error) { + return 'flat-danger'; + } + return isLoading ? 'flat-secondary' : 'flat-info'; +} + +interface PlanToSvgButtonProps { + plan: QueryPlan | ScriptPlan; + database: string; +} + +export function PlanToSvgButton({plan, database}: PlanToSvgButtonProps) { + const [error, setError] = React.useState(null); + const [blobUrl, setBlobUrl] = React.useState(null); + const [getPlanToSvg, {isLoading}] = planToSvgApi.usePlanToSvgQueryMutation(); + + const handleClick = React.useCallback(() => { + getPlanToSvg({plan, database}) + .unwrap() + .then((result) => { + const blob = new Blob([result], {type: 'image/svg+xml'}); + const url = URL.createObjectURL(blob); + setBlobUrl(url); + setError(null); + window.open(url, '_blank'); + }) + .catch((err) => { + setError(JSON.stringify(err)); + }); + }, [database, getPlanToSvg, plan]); + + React.useEffect(() => { + return () => { + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + }; + }, [blobUrl]); + + return ( + + + + ); +} diff --git a/src/containers/Tenant/Query/ExecuteResult/i18n/en.json b/src/containers/Tenant/Query/ExecuteResult/i18n/en.json index f4700c33d8..5bcdee8d35 100644 --- a/src/containers/Tenant/Query/ExecuteResult/i18n/en.json +++ b/src/containers/Tenant/Query/ExecuteResult/i18n/en.json @@ -7,5 +7,8 @@ "action.copy": "Copy {{activeSection}}", "trace": "Trace", "title.truncated": "Truncated", - "title.result": "Result" + "title.result": "Result", + "text_plan-svg": "Execution plan", + "text_open-plan-svg": "Open execution plan in new window", + "text_error-plan-svg": "Error: {{error}}" } diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index 3e43579a3e..0cc194e6ec 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -33,6 +33,9 @@ "settings.usePaginatedTables.title": "Use paginated tables", "settings.usePaginatedTables.description": " Use table with data load on scroll for Nodes and Storage tabs. It will increase performance, but could work unstable", + "settings.useShowPlanToSvg.title": "Plan to svg", + "settings.useShowPlanToSvg.description": " Show \"Plan to svg\" button in query result widow (if query was executed with full stats option).", + "settings.showDomainDatabase.title": "Show domain database", "settings.useClusterBalancerAsBackend.title": "Use cluster balancer as backend", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index ccd56768bf..583199b2c2 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -12,6 +12,7 @@ import { THEME_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, USE_PAGINATED_TABLES_KEY, + USE_SHOW_PLAN_SVG_KEY, } from '../../utils/constants'; import {Lang, defaultLang} from '../../utils/i18n'; @@ -96,6 +97,12 @@ export const usePaginatedTables: SettingProps = { description: i18n('settings.usePaginatedTables.description'), }; +export const useShowPlanToSvgTables: SettingProps = { + settingKey: USE_SHOW_PLAN_SVG_KEY, + title: i18n('settings.useShowPlanToSvg.title'), + description: i18n('settings.useShowPlanToSvg.description'), +}; + export const showDomainDatabase: SettingProps = { settingKey: SHOW_DOMAIN_DATABASE_KEY, title: i18n('settings.showDomainDatabase.title'), @@ -138,7 +145,7 @@ export const appearanceSection: SettingsSection = { export const experimentsSection: SettingsSection = { id: 'experimentsSection', title: i18n('section.experiments'), - settings: [usePaginatedTables], + settings: [usePaginatedTables, useShowPlanToSvgTables], }; export const devSettingsSection: SettingsSection = { id: 'devSettingsSection', diff --git a/src/services/api.ts b/src/services/api.ts index df8d7d0a98..f89b3b1b07 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -4,6 +4,7 @@ import type {AxiosRequestConfig} from 'axios'; import axiosRetry from 'axios-retry'; import {backend as BACKEND, metaBackend as META_BACKEND} from '../store'; +import type {PlanToSvgQueryParams} from '../store/reducers/planToSvg'; import type {TMetaInfo} from '../types/api/acl'; import type {TQueryAutocomplete} from '../types/api/autocomplete'; import type {CapabilitiesResponse} from '../types/api/capabilities'; @@ -578,6 +579,22 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } + planToSvg({database, plan}: PlanToSvgQueryParams, {signal}: {signal?: AbortSignal} = {}) { + return this.post( + this.getPath('/viewer/plan2svg'), + plan, + {database}, + { + requestConfig: { + signal, + responseType: 'text', + headers: { + Accept: 'image/svg+xml', + }, + }, + }, + ); + } getHotKeys( {path, database, enableSampling}: {path: string; database: string; enableSampling: boolean}, {concurrentId, signal}: AxiosOptions = {}, diff --git a/src/services/settings.ts b/src/services/settings.ts index 0695830364..3250650721 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -19,6 +19,7 @@ import { THEME_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, USE_PAGINATED_TABLES_KEY, + USE_SHOW_PLAN_SVG_KEY, } from '../utils/constants'; import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../utils/query'; import {parseJson} from '../utils/utils'; @@ -37,6 +38,7 @@ export const DEFAULT_USER_SETTINGS = { [ASIDE_HEADER_COMPACT_KEY]: true, [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], [USE_PAGINATED_TABLES_KEY]: true, + [USE_SHOW_PLAN_SVG_KEY]: false, [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, [ENABLE_AUTOCOMPLETE]: true, [AUTOCOMPLETE_ON_ENTER]: true, diff --git a/src/store/reducers/planToSvg.ts b/src/store/reducers/planToSvg.ts new file mode 100644 index 0000000000..2c34d78fb9 --- /dev/null +++ b/src/store/reducers/planToSvg.ts @@ -0,0 +1,31 @@ +import type {QueryPlan, ScriptPlan} from '../../types/api/query'; + +import {api} from './api'; + +export interface PlanToSvgQueryParams { + plan: ScriptPlan | QueryPlan; + database: string; +} + +export const planToSvgApi = api.injectEndpoints({ + endpoints: (build) => ({ + planToSvgQuery: build.mutation({ + queryFn: async ({plan, database}, {signal}) => { + try { + const response = await window.api.planToSvg( + { + database, + plan, + }, + {signal}, + ); + + return {data: response}; + } catch (error) { + return {error}; + } + }, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c591eeff9b..3f378d70a0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -134,6 +134,8 @@ export const TENANT_INITIAL_PAGE_KEY = 'saved_tenant_initial_tab'; // Old key value for backward compatibility export const USE_PAGINATED_TABLES_KEY = 'useBackendParamsForTables'; +export const USE_SHOW_PLAN_SVG_KEY = 'useShowPlanToSvg'; + // Setting to hide domain in database list export const SHOW_DOMAIN_DATABASE_KEY = 'showDomainDatabase';