From dff98db98f9b46521cbd9eae89eb0365eb45ccd0 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 10:30:17 +0300 Subject: [PATCH] feat(plugin-sql-editor): CB-1060 execution plan --- .../packages/core-app/public/icons/icons.svg | 16 +- webapp/packages/core-sdk/src/sdk.ts | 26 +-- .../src/ISqlEditorTabState.ts | 4 +- .../src/SqlEditor/SqlEditor.tsx | 17 +- .../src/SqlEditor/SqlEditorController.tsx | 14 +- .../src/SqlEditorTabService.ts | 12 +- .../ExecutionPlan/ExecutionPlanTree.tsx | 73 +++++++++ .../ExecutionPlan/ExecutionPlanTreeBlock.tsx | 63 ++++++++ .../ExecutionPlan/ExecutionPlanTreeContext.ts | 26 +++ .../SqlResultTabs/ExecutionPlan/Expand.tsx | 48 ++++++ .../ExecutionPlan/NestedNode.tsx | 100 ++++++++++++ .../ExecutionPlan/PropertiesPanel.tsx | 151 ++++++++++++++++++ .../ExecutionPlan/QueryPanel.tsx | 36 +++++ .../SQLExecutionPlanProcess.ts | 10 +- .../ExecutionPlan/SqlExecutionPlanPanel.tsx | 93 +++++++++++ .../ExecutionPlan/SqlExecutionPlanService.ts | 146 +++++++++++++++++ .../ExecutionPlan/getPropertyValue.ts | 23 +++ .../useExecutionPlanTreeState.ts | 85 ++++++++++ .../SqlResultTabs/SqlExecutionPlanService.ts | 108 ------------- .../src/SqlResultTabs/SqlQueryService.ts | 6 +- .../src/SqlResultTabs/SqlResultPanel.tsx | 3 +- .../src/SqlResultTabs/SqlResultTabs.tsx | 25 ++- .../src/SqlResultTabs/SqlResultTabsService.ts | 4 +- .../plugin-sql-editor/src/locales/en.ts | 6 + .../plugin-sql-editor/src/locales/ru.ts | 6 + .../plugin-sql-editor/src/manifest.ts | 2 +- 26 files changed, 942 insertions(+), 161 deletions(-) create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTree.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/Expand.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/PropertiesPanel.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx rename webapp/packages/plugin-sql-editor/src/SqlResultTabs/{ => ExecutionPlan}/SQLExecutionPlanProcess.ts (92%) create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/getPropertyValue.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts delete mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index c8c1bb4dd9..3c85ed29fc 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -183,7 +183,21 @@ - + + + + + + + + + + + + + + + diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 572765534d..515598eb00 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -806,7 +806,7 @@ export interface SqlCompletionProposal { replacementString: Scalars['String']; replacementOffset: Scalars['Int']; replacementLength: Scalars['Int']; - cursorPosition: Scalars['Int']; + cursorPosition?: Maybe; icon?: Maybe; nodePath?: Maybe; } @@ -1431,11 +1431,6 @@ export type ObjectOriginInfoFragment = ( & { details?: Maybe>> } ); -export type SqlExecutionPlanNodeFragment = ( - Pick - & { properties: Array> } -); - export type SessionStateFragment = Pick; export type UserConnectionAuthPropertiesFragment = Pick; @@ -1907,25 +1902,6 @@ export const NavNodeInfoFragmentDoc = ` } } ${NavNodePropertiesFragmentDoc}`; -export const SqlExecutionPlanNodeFragmentDoc = ` - fragment SQLExecutionPlanNode on SQLExecutionPlanNode { - kind - name - type - condition - description - properties { - id - category - dataType - description - displayName - features - value - order - } -} - `; export const SessionStateFragmentDoc = ` fragment SessionState on SessionInfo { createTime diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index e642523d65..7c47ad5d0a 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -37,8 +37,8 @@ export interface ISqlEditorResultTab { export interface IExecutionPlanTab { tabId: string; - query: string; executionContext: IDatabaseExecutionContext; + order: number; options?: Record; } @@ -50,5 +50,5 @@ export interface ISqlEditorTabState { tabs: ISqlEditorResultTab[]; resultGroups: IResultGroup[]; resultTabs: IResultTab[]; - executionPlanTabs: IExecutionPlanTab[]; // TODO: ex-plan store addition info for tab + executionPlanTabs: IExecutionPlanTab[]; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index bb59067aff..15fc0c3530 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -11,7 +11,7 @@ import { PropsWithChildren, useEffect, useRef } from 'react'; import styled, { css } from 'reshadow'; import { useTab } from '@cloudbeaver/core-app'; -import { StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks'; +import { Icon, StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import type { SQLCodeEditorController } from './SQLCodeEditor/SQLCodeEditorController'; @@ -44,13 +44,17 @@ const styles = css` background: none; } - StaticImage { + StaticImage, icon-container { padding: 4px; height: 24px; width: 24px; cursor: pointer; } + icon-container { + display: flex; + } + SQLCodeEditorLoader { flex: 1; } @@ -102,11 +106,16 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd /> => { + if (this.isActionsDisabled) { + return; + } this.sqlQueryService.executeEditorQuery( this.sqlResultTabsService.getTabExecutionContext(this.tab.id), this.tab.handlerState, @@ -47,6 +50,9 @@ export class SqlEditorController implements IInitializableController { }; handleExecuteNewTab = async (): Promise => { + if (this.isActionsDisabled) { + return; + } this.sqlQueryService.executeEditorQuery( this.sqlResultTabsService.getTabExecutionContext(this.tab.id), this.tab.handlerState, @@ -56,6 +62,9 @@ export class SqlEditorController implements IInitializableController { }; handleExecutionPlan = async (): Promise => { + if (this.isActionsDisabled) { + return; + } this.sqlExecutionPlanService.executeExecutionPlan( this.sqlResultTabsService.getTabExecutionContext(this.tab.id), this.tab.handlerState, @@ -63,8 +72,6 @@ export class SqlEditorController implements IInitializableController { ); }; - // TODO: ex-plan add action for execution plan - readonly options: EditorConfiguration = { theme: 'material', lineNumbers: true, @@ -80,6 +87,7 @@ export class SqlEditorController implements IInitializableController { // Execute sql script in new tab 'Ctrl-\\': () => { this.handleExecuteNewTab(); }, 'Shift-Ctrl-Enter': () => { this.handleExecuteNewTab(); }, + 'Shift-Ctrl-E': () => { this.handleExecutionPlan(); }, // Autocomplete 'Ctrl-Space': this.showHint.bind(this), // classic for windows, linux diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts index 929357cd99..d98183c547 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts @@ -246,13 +246,13 @@ export class SqlEditorTabService extends Bootstrap { } } - private async handleTabClose(tab: ITab) { - this.tabExecutionState.delete(tab.id); - if (tab.handlerState.executionContext) { - await this.sqlEditorService.destroySqlContext(tab.handlerState.executionContext); + private async handleTabClose(editorTab: ITab) { + this.tabExecutionState.delete(editorTab.id); + if (editorTab.handlerState.executionContext) { + await this.sqlEditorService.destroySqlContext(editorTab.handlerState.executionContext); } - for (const resultTab of tab.handlerState.resultTabs) { - await this.sqlResultTabsService.removeResultTab(tab.handlerState, resultTab.tabId); + for (const tab of editorTab.handlerState.tabs) { + await this.sqlResultTabsService.removeResultTab(editorTab.handlerState, tab.id); } } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTree.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTree.tsx new file mode 100644 index 0000000000..4dc7fc4c2c --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTree.tsx @@ -0,0 +1,73 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observable } from 'mobx'; +import { useCallback, useContext, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { ExecutionPlanTreeContext, IExecutionPlanNode } from './ExecutionPlanTreeContext'; +import { NestedNode } from './NestedNode'; + +const styles = composes( + css` + TableHeader { + composes: theme-background-surface from global; + } + `, + css` + TableHeader { + position: sticky; + top: 0; + z-index: 1; + } + ` +); + +interface Props { + className?: string; +} + +export const ExecutionPlanTree: React.FC = function ExecutionPlanTree({ className }) { + const treeContext = useContext(ExecutionPlanTreeContext); + const [selectedNodes] = useState(() => observable(new Map())); + + if (!treeContext) { + throw new Error('Tree context must be provided'); + } + + const selectNode = useCallback((node: IExecutionPlanNode) => { + selectedNodes.clear(); + selectedNodes.set(node, true); + + treeContext.selectNode(node); + }, [treeContext, selectedNodes]); + + return styled(useStyles(styles))( + + + {treeContext.columns.map(property => ( + + {property.displayName} + + ))} + + + {treeContext.nodes.map(node => ( + + ))} + +
+ ); +}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx new file mode 100644 index 0000000000..b2360c0368 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx @@ -0,0 +1,63 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useContext } from 'react'; +import styled, { css } from 'reshadow'; + +import { Split, Pane, ResizerControls, splitStyles } from '@cloudbeaver/core-blocks'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { ExecutionPlanTree } from './ExecutionPlanTree'; +import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; +import { QueryPanel } from './QueryPanel'; + +const styles = composes( + css` + Pane { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Split { + height: 100%; + flex-direction: column; + } + ResizerControls { + width: 100%; + height: 2px; + } + Pane:last-child { + flex: 0 0 30%; + } + ` +); + +interface Props { + className?: string; +} + +export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTreeBlock({ className }) { + const style = useStyles(styles, splitStyles); + const treeContext = useContext(ExecutionPlanTreeContext); + + if (!treeContext) { + throw new Error('Tree context must be provided'); + } + + return styled(style)( + + + + + + + + + + ); +}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts new file mode 100644 index 0000000000..2279ecb5f8 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { createContext } from 'react'; + +import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk'; + +export interface IExecutionPlanNode extends SqlExecutionPlanNode { + children: IExecutionPlanNode[]; +} + +export interface IExecutionPlanTreeContext { + query: string; + columns: ObjectPropertyInfo[]; + nodes: IExecutionPlanNode[]; + selectNode: (node: IExecutionPlanNode) => void; + selectedNode: IExecutionPlanNode | null; + metadataPanel: boolean; +} + +export const ExecutionPlanTreeContext = createContext(null); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/Expand.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/Expand.tsx new file mode 100644 index 0000000000..c9fe4856ae --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/Expand.tsx @@ -0,0 +1,48 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import styled, { css, use } from 'reshadow'; + +import { Icon } from '@cloudbeaver/core-blocks'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +interface Props { + expanded: boolean; + onClick: (event: React.MouseEvent) => void; + className?: string; +} + +const styles = composes( + css` + Icon { + composes: theme-text-on-surface from global; + } +`, + css` + Icon { + cursor: pointer; + opacity: 0.5; + transform: rotate(-90deg); + transition: transform 0.15s ease-in-out; + &[|expanded] { + transform: rotate(0deg); + } + } + `); + +export const Expand: React.FC = function Expand({ expanded, onClick, className }) { + return styled(useStyles(styles))( + + ); +}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx new file mode 100644 index 0000000000..3eae668539 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx @@ -0,0 +1,100 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useCallback, useContext, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { TableItem, TableColumnValue } from '@cloudbeaver/core-blocks'; +import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { ExecutionPlanTreeContext, IExecutionPlanNode } from './ExecutionPlanTreeContext'; +import { Expand } from './Expand'; +import { getPropertyValue } from './getPropertyValue'; + +const styles = css` + TableColumnValue { + white-space: pre; + } + expand-container { + width: 24px; + height: 24px; + margin-right: 4px; + display: flex; + } + control { + display: flex; + align-items: center; + } +`; + +interface Props { + node: IExecutionPlanNode; + depth: number; + className?: string; +} + +export const NestedNode: React.FC = function NestedNode({ node, depth, className }) { + const style = useStyles(styles); + const treeContext = useContext(ExecutionPlanTreeContext); + const [expanded, setExpanded] = useState(true); + + if (!treeContext) { + throw new Error('Tree context must be provided'); + } + + const getValue = useCallback((property: ObjectPropertyInfo | undefined) => { + if (!property || !treeContext.columns.find(column => column.id === property.id)) { + return ''; + } + + return getPropertyValue(property); + }, [treeContext.columns]); + + const expand = useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + setExpanded(prev => !prev); + }, []); + + const hasChildren = node.children.length > 0; + + return styled(style)( + <> + + {treeContext.columns.map((column, idx) => { + const property = node.properties.find(property => property.id === column.id); + const value = getValue(property); + return ( + + + {idx === 0 && ( + <> + {`${'\t'.repeat(depth)}`} + + {hasChildren && ( + + )} + + + )} + {value} + + + ); + })} + + {expanded && node.children.map(child => ( + + ))} + + ); +}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/PropertiesPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/PropertiesPanel.tsx new file mode 100644 index 0000000000..63566f3434 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/PropertiesPanel.tsx @@ -0,0 +1,151 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useMemo, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { + Table, TableHeader, TableColumnHeader, TableBody, + TableItem, + TableColumnValue, + TextPlaceholder, +} from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { Expand } from './Expand'; +import { getPropertyValue } from './getPropertyValue'; +import { isVisibleProperty } from './useExecutionPlanTreeState'; + +const styles = composes( + css` + TableColumnHeader { + composes: theme-background-surface from global; + } + `, + css` + TableColumnHeader { + position: sticky; + top: 0; + z-index: 1; + } + TableColumnValue { + white-space: pre; + } + control { + display: flex; + align-items: center; + } + expand-container { + width: 24px; + height: 24px; + margin-right: 4px; + cursor: pointer; + display: flex; + } + ` +); + +interface Props { + properties: ObjectPropertyInfo[]; + className?: string; +} + +export const PropertiesPanel: React.FC = observer(function PropertiesPanel({ properties, className }) { + const style = useStyles(styles); + const translate = useTranslate(); + const [generalExpanded, setGeneralExpanded] = useState(true); + const [detailsExpanded, setDetailsExpanded] = useState(true); + + const { general, details } = useMemo(() => computed(() => { + const general: ObjectPropertyInfo[] = []; + const details: ObjectPropertyInfo[] = []; + + for (const property of properties) { + if (isVisibleProperty(property)) { + general.push(property); + } else { + details.push(property); + } + } + + return { general, details }; + }), [properties]).get(); + + if (!general.length && !details.length) { + return {translate('sql_execution_plan_properties_panel_placeholder')}; + } + + return styled(style)( + + + + {translate('sql_execution_plan_properties_panel_name')} + + + {translate('sql_execution_plan_properties_panel_value')} + + + + + + + + setGeneralExpanded(!generalExpanded)} /> + + {translate('sql_execution_plan_properties_panel_general')} + + + + + {generalExpanded && general.map(property => { + const name = property.displayName; + const value = getPropertyValue(property); + return ( + + + {'\t\t' + name} + + + {value} + + + ); + })} + + + + + setDetailsExpanded(!detailsExpanded)} /> + + {translate('sql_execution_plan_properties_panel_details')} + + + + + {detailsExpanded && details.map(property => { + const name = property.displayName; + const value = getPropertyValue(property); + return ( + + + {'\t\t' + name} + + + {value} + + + ); + })} + +
+ ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx new file mode 100644 index 0000000000..4bcc0e798f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useContext } from 'react'; + +import { TextareaNew } from '@cloudbeaver/core-blocks'; + +import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; + +interface Props { + className?: string; +} + +export const QueryPanel: React.FC = function QueryPanel({ className }) { + const treeContext = useContext(ExecutionPlanTreeContext); + + if (!treeContext) { + throw new Error('Tree context must be provided'); + } + + return ( + + ); +}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLExecutionPlanProcess.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SQLExecutionPlanProcess.ts similarity index 92% rename from webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLExecutionPlanProcess.ts rename to webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SQLExecutionPlanProcess.ts index 49184c45b6..14f853053b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLExecutionPlanProcess.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SQLExecutionPlanProcess.ts @@ -8,7 +8,7 @@ import type { NotificationService } from '@cloudbeaver/core-events'; import { - AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecuteInfo, SqlDataFilter, ResultDataFormat, SqlExecutionPlan + AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecutionPlan } from '@cloudbeaver/core-sdk'; import { CancellablePromise, cancellableTimeout, Deferred, EDeferredState @@ -35,7 +35,7 @@ export class SQLExecutionPlanProcess extends Deferred { ): Promise { // start async task try { - const taskInfo = await this.executeQueryAsync(query, context); + const taskInfo = await this.explainExecutionPlanAsync(query, context); await this.applyResult(taskInfo); this.taskId = taskInfo.id; if (this.getState() === EDeferredState.CANCELLING) { @@ -57,7 +57,7 @@ export class SQLExecutionPlanProcess extends Deferred { } // run the first check immediately because usually the query execution is fast try { - const taskInfo = await this.getQueryStatusAsync(this.taskId); + const taskInfo = await this.getExecutionPlanStatusAsync(this.taskId); await this.applyResult(taskInfo); if (this.isFinished) { return; @@ -103,7 +103,7 @@ export class SQLExecutionPlanProcess extends Deferred { } } - private async executeQueryAsync( + private async explainExecutionPlanAsync( query: string, context: IDatabaseExecutionContext, ): Promise { @@ -116,7 +116,7 @@ export class SQLExecutionPlanProcess extends Deferred { return taskInfo; } - private async getQueryStatusAsync(taskId: string): Promise { + private async getExecutionPlanStatusAsync(taskId: string): Promise { const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId, removeOnFinish: false }); return taskInfo; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx new file mode 100644 index 0000000000..a2ba06e80c --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx @@ -0,0 +1,93 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useCallback } from 'react'; +import styled, { css } from 'reshadow'; + +import { Split, Pane, ResizerControls, splitStyles, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { Loader } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { EDeferredState } from '@cloudbeaver/core-utils'; + +import type { IExecutionPlanTab } from '../../ISqlEditorTabState'; +import { ExecutionPlanTreeBlock } from './ExecutionPlanTreeBlock'; +import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; +import { PropertiesPanel } from './PropertiesPanel'; +import { SqlExecutionPlanService } from './SqlExecutionPlanService'; +import { useExecutionPlanTreeState } from './useExecutionPlanTreeState'; + +const styles = composes( + css` + Pane { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Split { + height: 100%; + } + ` +); + +interface Props { + executionPlanTab: IExecutionPlanTab; +} + +export const SqlExecutionPlanPanel: React.FC = observer(function SqlExecutionPlanPanel({ + executionPlanTab, +}) { + const style = useStyles(styles, splitStyles); + const translate = useTranslate(); + const sqlExecutionPlanService = useService(SqlExecutionPlanService); + const data = sqlExecutionPlanService.results.get(executionPlanTab.tabId); + const process = sqlExecutionPlanService.processes.get(executionPlanTab.tabId); + + const executionPlanState = useExecutionPlanTreeState(data?.executionPlan.nodes || [], data?.executionPlan.query || ''); + const loading = process?.isInProgress || !data; + const canCancel = process ? process?.getState() === EDeferredState.PENDING : false; + + const cancelTask = useCallback(() => { + if (process) { + process.cancel(); + } + }, [process]); + + if (loading) { + return ( + + ); + } + + if (!executionPlanState.nodes.length || !executionPlanState.columns.length) { + return {translate('sql_execution_plan_placeholder')}; + } + + return styled(style)( + + + + + + {executionPlanState.metadataPanel && executionPlanState.selectedNode && ( + <> + + + + + + )} + + + ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts new file mode 100644 index 0000000000..5c4e754343 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts @@ -0,0 +1,146 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { makeObservable, observable, toJS } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { GraphQLService, SqlExecutionPlan } from '@cloudbeaver/core-sdk'; +import { EDeferredState, uuid } from '@cloudbeaver/core-utils'; + +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import { SqlDialectInfoService } from '../../SqlDialectInfoService'; +import type { SqlExecutionState } from '../../SqlExecutionState'; +import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess'; + +interface IExecutionPlanData { + query: string; + executionPlan: SqlExecutionPlan; +} +@injectable() +export class SqlExecutionPlanService { + results: Map; + processes: Map; + + constructor( + private sqlDialectInfoService: SqlDialectInfoService, + private graphQLService: GraphQLService, + private notificationService: NotificationService, + ) { + makeObservable(this, { + results: observable, + processes: observable, + }); + this.results = new Map(); + this.processes = new Map(); + } + + async executeExecutionPlan( + executionState: SqlExecutionState, + editorState: ISqlEditorTabState, + query: string, + ): Promise { + if (!editorState.executionContext) { + console.error('executeEditorQuery executionContext is not provided'); + return; + } + + const tabId = this.createExecutionPlanTab(editorState); + const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query); + const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService); + + this.processes.set(tabId, task); + executionState.setExecutionTask(task); + editorState.currentTabId = tabId; + + try { + await task.start( + subQuery, + { + connectionId: editorState.executionContext?.connectionId, + contextId: editorState.executionContext?.contextId, + } + ); + + const executionPlan = await task.promise; + this.results.set(tabId, { + executionPlan, + query, + }); + } catch (exception) { + const message = task.getState() === EDeferredState.CANCELLED ? 'Execution plan process has been canceled' : undefined; + this.notificationService.logException(exception, 'Execution plan Error', message); + this.removeTab(editorState, tabId); + } + } + + async getSubQuery(connectionId: string, query: string): Promise { + const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId); + + if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) { + return query.slice(0, query.length - dialectInfo.scriptDelimiter.length); + } + + return query; + } + + private removeTab(state: ISqlEditorTabState, tabId: string) { + this.removeExecutionPlanTab(state, tabId); + + if (state.tabs.length > 0) { + state.currentTabId = state.tabs[0].id; + } else { + state.currentTabId = ''; + } + } + + removeExecutionPlanTab(state: ISqlEditorTabState, tabId: string): void { + const tab = state.tabs.find(tab => tab.id === tabId); + const executionPlanTab = state.executionPlanTabs.find(executionPlanTab => executionPlanTab.tabId === tabId); + + if (executionPlanTab) { + state.executionPlanTabs.splice(state.executionPlanTabs.indexOf(executionPlanTab), 1); + } + + if (tab) { + state.tabs.splice(state.tabs.indexOf(tab), 1); + } + + this.results.delete(tabId); + this.processes.delete(tabId); + } + + private createExecutionPlanTab( + state: ISqlEditorTabState, + ) { + if (!state.executionContext) { + throw new Error('executeEditorQuery executionContext is not provided'); + } + + const id = uuid(); + const order = Math.max(0, ...state.executionPlanTabs.map(tab => tab.order + 1)); + + state.executionPlanTabs.push({ + tabId: id, + executionContext: { + connectionId: state.executionContext.connectionId, + contextId: state.executionContext.contextId, + }, + order, + }); + + state.tabs.push({ + id, + name: `Execution plan - ${order + 1}`, + icon: 'execution-plan-tab', + order, + }); + + return id; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/getPropertyValue.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/getPropertyValue.ts new file mode 100644 index 0000000000..9ebf1101ea --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/getPropertyValue.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; + +export function getPropertyValue(property: ObjectPropertyInfo): string { + const value = property.value; + + if (!value) { + return ''; + } + + if (value !== null && typeof value === 'object') { + return value.displayValue || value.fullName || ''; + } + + return value; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts new file mode 100644 index 0000000000..4de07a8b4e --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts @@ -0,0 +1,85 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed, observable } from 'mobx'; +import { useCallback, useMemo, useState } from 'react'; + +import { useObjectRef } from '@cloudbeaver/core-blocks'; +import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk'; + +import type { IExecutionPlanNode, IExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; + +interface IState { + selectedNode: IExecutionPlanNode | null; + metadataPanel: boolean; +} + +export function isVisibleProperty(property: ObjectPropertyInfo): boolean { + return property.features.includes('viewable'); +} + +export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[], query: string): IExecutionPlanTreeContext { + const props = useObjectRef({ list, query }); + const [state] = useState(() => observable({ + selectedNode: null, + metadataPanel: false, + })); + + const selectNode = useCallback((node: IExecutionPlanNode) => { + if (!state.metadataPanel) { + state.metadataPanel = true; + } + state.selectedNode = node; + }, [state]); + + const columns = useMemo(() => computed(() => { + const result: ObjectPropertyInfo[] = []; + + for (const node of props.list) { + for (const property of node.properties) { + if (property.id && isVisibleProperty(property) && !result.find(column => column.id === property.id)) { + result.push(property); + } + } + } + + return result; + }), [props.list]).get(); + + const nodes = useMemo(() => computed(() => { + const map: Map = new Map(); + + const tree: IExecutionPlanNode[] = props.list + .map((node, idx) => { + map.set(node.id, idx); + return ({ ...node, children: [] }); + }); + + const result: IExecutionPlanNode[] = []; + + for (const node of tree) { + if (node.parentId) { + const parent = map.get(node.parentId)!; + tree[parent].children.push(node); + } else { + result.push(node); + } + } + + return result; + }), [props.list]).get(); + + return useObjectRef({ + columns, + nodes, + metadataPanel: state.metadataPanel, + selectedNode: state.selectedNode, + query: props.query, + selectNode, + }); +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts deleted file mode 100644 index 6043f5e1a1..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; -import { uuid } from '@cloudbeaver/core-utils'; -import { TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer'; - -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { SqlDialectInfoService } from '../SqlDialectInfoService'; -import type { SqlExecutionState } from '../SqlExecutionState'; -import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess'; - -@injectable() -export class SqlExecutionPlanService { - constructor( - private sqlDialectInfoService: SqlDialectInfoService, - private tableViewerStorageService: TableViewerStorageService, - private graphQLService: GraphQLService, - private notificationService: NotificationService, - ) { } - - async executeExecutionPlan( - executionState: SqlExecutionState, - editorState: ISqlEditorTabState, - query: string, - ): Promise { - if (!editorState.executionContext) { - console.error('executeEditorQuery executionContext is not provided'); - return; - } - - const process = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService); - await process.start(await this.getSubQuery(editorState.executionContext.connectionId, query), - { - connectionId: editorState.executionContext?.connectionId, - contextId: editorState.executionContext?.contextId, - } - ); - - const result = await process.promise; - - this.createExecutionPlanTab(editorState, await this.getSubQuery(editorState.executionContext.connectionId, query)); - - console.log(result); - } - - async getSubQuery(connectionId: string, query: string): Promise { - const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId); - - if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) { - return query.slice(0, query.length - dialectInfo.scriptDelimiter.length); - } - - return query; - } - - removeResultTab(state: ISqlEditorTabState, tabId: string): void { - const resultTab = state.resultTabs.find(resultTab => resultTab.tabId === tabId); - const group = state.resultGroups.find(group => group.groupId === resultTab?.groupId); - - if (resultTab && group) { - state.resultTabs.splice(state.resultTabs.indexOf(resultTab), 1); - - const isGroupEmpty = !state.resultTabs.some(resultTab => resultTab.groupId === group.groupId); - - if (isGroupEmpty) { - state.resultGroups.splice(state.resultGroups.indexOf(group), 1); - // TODO: probably we should cleanup some data before model delete - this.tableViewerStorageService.remove(group.modelId); - } - } - } - - private createExecutionPlanTab( - state: ISqlEditorTabState, - query: string, - ) { - if (!state.executionContext) { - console.error('executeEditorQuery executionContext is not provided'); - return; - } - - const id = uuid(); - - state.executionPlanTabs.push({ - tabId: id, - executionContext: { - connectionId: state.executionContext.connectionId, - contextId: state.executionContext.contextId, - }, - query, - }); - - state.tabs.push({ - id, - name: 'Execution plan', - icon: '/icons/grid.png', - order: state.executionPlanTabs.length + 1, - }); - } -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts index 2550cb9f5c..4559899a1e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts @@ -131,7 +131,11 @@ export class SqlQueryService { tabState: ISqlEditorTabState, modelId: string ): IResultGroup { - const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1)); + const executionPlanIds = tabState.executionPlanTabs.map(tab => tab.tabId); + const order = Math.max(0, ...tabState.tabs + .filter(tab => !executionPlanIds.includes(tab.id)) + .map(tab => tab.order + 1) + ); const groupId = uuid(); tabState.resultGroups.push({ diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx index d43d63ed6e..a10a29f6e0 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx @@ -12,6 +12,7 @@ import styled, { css } from 'reshadow'; import type { ITab } from '@cloudbeaver/core-app'; import type { ISqlEditorTabState } from '../ISqlEditorTabState'; +import { SqlExecutionPlanPanel } from './ExecutionPlan/SqlExecutionPlanPanel'; import { SqlResultSetPanel } from './SqlResultSetPanel'; const style = css` @@ -44,7 +45,7 @@ export const SqlResultPanel = observer(function SqlResultPanel({ tab, id }: SqlR if (executionPlanTab) { return styled(style)( -
{executionPlanTab.query}
+
); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index 0638adf8c8..f0348ef372 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -28,12 +28,16 @@ const styles = composes( Tab { composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; } + TabIcon { + composes: theme-text-surface from global; + } tabs { composes: theme-background-background theme-text-text-primary-on-light from global; } `, css` wrapper { + overflow: auto; display: flex; flex: 1; height: 100%; @@ -72,10 +76,25 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes [tab] ); + const categorizedTabs = useMemo(() => computed(() => { + const resultTabs = []; + const executionPlanTabs = []; + + for (const orderedTab of orderedTabs.get()) { + const resultTab = tab.handlerState.resultTabs.find(tab => tab.tabId === orderedTab.id); + if (resultTab) { + resultTabs.push(orderedTab); + continue; + } + executionPlanTabs.push(orderedTab); + } + return resultTabs.concat(executionPlanTabs); + }), [orderedTabs, tab]).get(); + const handleOpen = ({ tabId }: ITabData) => navigatorService.openEditorResult(tab.id, tabId); const handleClose = ({ tabId }: ITabData) => navigatorService.closeEditorResult(tab.id, tabId); - if (!tab.handlerState.resultGroups.length) { + if (!tab.handlerState.tabs.length) { return {translate('sql_editor_placeholder')}; } @@ -85,7 +104,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes ( + tabs={categorizedTabs.map(result => ( {result.name} @@ -93,7 +112,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes ))} style={[styles]} > - {orderedTabs.get().map(result => ( + {categorizedTabs.map(result => ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 5b4252372d..5067db8fa9 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -11,6 +11,7 @@ import { MetadataMap } from '@cloudbeaver/core-utils'; import type { ISqlEditorTabState } from '../ISqlEditorTabState'; import { SqlExecutionState } from '../SqlExecutionState'; +import { SqlExecutionPlanService } from './ExecutionPlan/SqlExecutionPlanService'; import { SqlQueryService } from './SqlQueryService'; @injectable() @@ -19,6 +20,7 @@ export class SqlResultTabsService { constructor( private sqlQueryService: SqlQueryService, + private sqlExecutionPlanService: SqlExecutionPlanService ) { this.tabExecutionContext = new MetadataMap(() => new SqlExecutionState()); } @@ -35,7 +37,7 @@ export class SqlResultTabsService { } this.sqlQueryService.removeResultTab(state, tabId); - // TODO: ex-plan add removeResultTab for ex-plan + this.sqlExecutionPlanService.removeExecutionPlanTab(state, tabId); if (state.currentTabId === tabId) { if (state.tabs.length > 0) { diff --git a/webapp/packages/plugin-sql-editor/src/locales/en.ts b/webapp/packages/plugin-sql-editor/src/locales/en.ts index adcf1920df..d5f3865d62 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/en.ts @@ -1,3 +1,9 @@ export default [ ['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'], + ['sql_execution_plan_placeholder', 'Execution plan is empty'], + ['sql_execution_plan_properties_panel_placeholder', 'There are no items to show'], + ['sql_execution_plan_properties_panel_name', 'Name'], + ['sql_execution_plan_properties_panel_value', 'Value'], + ['sql_execution_plan_properties_panel_general', 'General'], + ['sql_execution_plan_properties_panel_details', 'Details'], ]; diff --git a/webapp/packages/plugin-sql-editor/src/locales/ru.ts b/webapp/packages/plugin-sql-editor/src/locales/ru.ts index 4bed43b963..c18f1ac829 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/ru.ts @@ -1,3 +1,9 @@ export default [ ['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'], + ['sql_execution_plan_placeholder', 'План выполнения пустой'], + ['sql_execution_plan_properties_panel_placeholder', 'Информация не доступна'], + ['sql_execution_plan_properties_panel_name', 'Название'], + ['sql_execution_plan_properties_panel_value', 'Значение'], + ['sql_execution_plan_properties_panel_general', 'Общие'], + ['sql_execution_plan_properties_panel_details', 'Детали'], ]; diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index 01483533fe..afbc9e5f61 100644 --- a/webapp/packages/plugin-sql-editor/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor/src/manifest.ts @@ -14,7 +14,7 @@ import { SqlEditorBootstrap } from './SqlEditorBootstrap'; import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; import { SqlEditorService } from './SqlEditorService'; import { SqlEditorTabService } from './SqlEditorTabService'; -import { SqlExecutionPlanService } from './SqlResultTabs/SqlExecutionPlanService'; +import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService'; import { SqlQueryService } from './SqlResultTabs/SqlQueryService'; import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';