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';