diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg
index 43c5864989..e70ea3855a 100644
--- a/webapp/packages/core-app/public/icons/icons.svg
+++ b/webapp/packages/core-app/public/icons/icons.svg
@@ -183,7 +183,7 @@
-
+
@@ -192,4 +192,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/packages/core-sdk/src/queries/grid/asyncSqlExplainExecutionPlan.gql b/webapp/packages/core-sdk/src/queries/grid/asyncSqlExplainExecutionPlan.gql
new file mode 100644
index 0000000000..1c7ea949f8
--- /dev/null
+++ b/webapp/packages/core-sdk/src/queries/grid/asyncSqlExplainExecutionPlan.gql
@@ -0,0 +1,24 @@
+mutation asyncSqlExplainExecutionPlan(
+ $connectionId: ID!
+ $contextId: ID!
+ $query: String!
+ $configuration: Object!
+) {
+ taskInfo: asyncSqlExplainExecutionPlan(
+ connectionId: $connectionId
+ contextId: $contextId
+ query: $query
+ configuration: $configuration
+ ) {
+ id
+ name
+ running
+ status
+ error {
+ message
+ errorCode
+ stackTrace
+ }
+ taskResult
+ }
+}
diff --git a/webapp/packages/core-sdk/src/queries/grid/getSqlExecutionPlanResult.gql b/webapp/packages/core-sdk/src/queries/grid/getSqlExecutionPlanResult.gql
new file mode 100644
index 0000000000..80d522a914
--- /dev/null
+++ b/webapp/packages/core-sdk/src/queries/grid/getSqlExecutionPlanResult.gql
@@ -0,0 +1,28 @@
+mutation getSqlExecutionPlanResult(
+ $taskId: ID!
+) {
+ result: asyncSqlExplainExecutionPlanResult(
+ taskId: $taskId
+ ) {
+ query
+ nodes {
+ id
+ parentId
+ kind
+ name
+ type
+ condition
+ description
+ properties {
+ id
+ category
+ dataType
+ description
+ displayName
+ features
+ value
+ order
+ }
+ }
+ }
+}
diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts
index 48581c9f42..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;
}
@@ -1488,6 +1488,20 @@ export interface AsyncSqlExecuteQueryMutation {
);
}
+export type AsyncSqlExplainExecutionPlanMutationVariables = Exact<{
+ connectionId: Scalars['ID'];
+ contextId: Scalars['ID'];
+ query: Scalars['String'];
+ configuration: Scalars['Object'];
+}>;
+
+export interface AsyncSqlExplainExecutionPlanMutation {
+ taskInfo: (
+ Pick
+ & { error?: Maybe> }
+ );
+}
+
export type GetSqlExecuteTaskResultsMutationVariables = Exact<{
taskId: Scalars['ID'];
}>;
@@ -1508,6 +1522,20 @@ export interface GetSqlExecuteTaskResultsMutation {
);
}
+export type GetSqlExecutionPlanResultMutationVariables = Exact<{
+ taskId: Scalars['ID'];
+}>;
+
+export interface GetSqlExecutionPlanResultMutation {
+ result: (
+ Pick
+ & { nodes: Array<(
+ Pick
+ & { properties: Array> }
+ )>; }
+ );
+}
+
export type UpdateResultsDataMutationVariables = Exact<{
connectionId: Scalars['ID'];
contextId: Scalars['ID'];
@@ -2383,6 +2411,27 @@ export const AsyncSqlExecuteQueryDocument = `
}
}
`;
+export const AsyncSqlExplainExecutionPlanDocument = `
+ mutation asyncSqlExplainExecutionPlan($connectionId: ID!, $contextId: ID!, $query: String!, $configuration: Object!) {
+ taskInfo: asyncSqlExplainExecutionPlan(
+ connectionId: $connectionId
+ contextId: $contextId
+ query: $query
+ configuration: $configuration
+ ) {
+ id
+ name
+ running
+ status
+ error {
+ message
+ errorCode
+ stackTrace
+ }
+ taskResult
+ }
+}
+ `;
export const GetSqlExecuteTaskResultsDocument = `
mutation getSqlExecuteTaskResults($taskId: ID!) {
result: asyncSqlExecuteResults(taskId: $taskId) {
@@ -2423,6 +2472,32 @@ export const GetSqlExecuteTaskResultsDocument = `
}
}
`;
+export const GetSqlExecutionPlanResultDocument = `
+ mutation getSqlExecutionPlanResult($taskId: ID!) {
+ result: asyncSqlExplainExecutionPlanResult(taskId: $taskId) {
+ query
+ nodes {
+ id
+ parentId
+ kind
+ name
+ type
+ condition
+ description
+ properties {
+ id
+ category
+ dataType
+ description
+ displayName
+ features
+ value
+ order
+ }
+ }
+ }
+}
+ `;
export const UpdateResultsDataDocument = `
mutation updateResultsData($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $sourceRowValues: [Object]!, $values: Object) {
result: updateResultsData(
@@ -2833,9 +2908,15 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
asyncSqlExecuteQuery(variables: AsyncSqlExecuteQueryMutationVariables): Promise {
return withWrapper(() => client.request(AsyncSqlExecuteQueryDocument, variables));
},
+ asyncSqlExplainExecutionPlan(variables: AsyncSqlExplainExecutionPlanMutationVariables): Promise {
+ return withWrapper(() => client.request(AsyncSqlExplainExecutionPlanDocument, variables));
+ },
getSqlExecuteTaskResults(variables: GetSqlExecuteTaskResultsMutationVariables): Promise {
return withWrapper(() => client.request(GetSqlExecuteTaskResultsDocument, variables));
},
+ getSqlExecutionPlanResult(variables: GetSqlExecutionPlanResultMutationVariables): Promise {
+ return withWrapper(() => client.request(GetSqlExecutionPlanResultDocument, variables));
+ },
updateResultsData(variables: UpdateResultsDataMutationVariables): Promise {
return withWrapper(() => client.request(UpdateResultsDataDocument, variables));
},
diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
index e642523d65..467e88fe17 100644
--- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
+++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
@@ -26,6 +26,7 @@ export interface IResultGroup {
modelId: string;
sqlQueryParams: ISqlQueryParams;
order: number;
+ nameOrder: number;
}
export interface ISqlEditorResultTab {
@@ -37,8 +38,9 @@ export interface ISqlEditorResultTab {
export interface IExecutionPlanTab {
tabId: string;
- query: string;
executionContext: IDatabaseExecutionContext;
+ query: string;
+ order: number;
options?: Record;
}
@@ -50,5 +52,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 2e3d8469ac..c4f8b54a55 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx
@@ -11,8 +11,9 @@ 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 { useTranslate } from '@cloudbeaver/core-localization';
import type { SQLCodeEditorController } from './SQLCodeEditor/SQLCodeEditorController';
import { SQLCodeEditorLoader } from './SQLCodeEditor/SQLCodeEditorLoader';
@@ -44,7 +45,7 @@ const styles = css`
background: none;
}
- StaticImage {
+ StaticImage, Icon {
padding: 4px;
height: 24px;
width: 24px;
@@ -57,6 +58,7 @@ const styles = css`
`;
export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEditorProps) {
+ const translate = useTranslate();
const tab = useTab(tabId);
const editor = useRef(null);
const baseTab = useBaseTab(tabId);
@@ -79,8 +81,8 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd
}
return styled(styles)(
-
-
+
+
+
=> {
+ if (this.isActionsDisabled) {
+ return;
+ }
this.sqlQueryService.executeEditorQuery(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
@@ -46,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,
@@ -54,7 +61,16 @@ export class SqlEditorController implements IInitializableController {
);
};
- // TODO: ex-plan add action for execution plan
+ handleExecutionPlan = async (): Promise => {
+ if (this.isActionsDisabled) {
+ return;
+ }
+ this.sqlExecutionPlanService.executeExecutionPlan(
+ this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
+ this.tab.handlerState,
+ await this.getExecutingQuery(),
+ );
+ };
readonly options: EditorConfiguration = {
theme: 'material',
@@ -71,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
@@ -96,7 +113,8 @@ export class SqlEditorController implements IInitializableController {
private sqlResultTabsService: SqlResultTabsService,
private sqlQueryService: SqlQueryService,
private sqlDialectInfoService: SqlDialectInfoService,
- private sqlEditorService: SqlEditorService
+ private sqlEditorService: SqlEditorService,
+ private sqlExecutionPlanService: SqlExecutionPlanService,
) {
makeObservable(this, {
dialect: computed,
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..0fa206ac4e
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTree.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks';
+import { composes, useStyles } from '@cloudbeaver/core-theming';
+
+import { ExecutionPlanTreeContext } 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);
+
+ if (!treeContext) {
+ throw new Error('Tree context must be provided');
+ }
+
+ 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..281754417c
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 } from 'reshadow';
+
+import { Split, Pane, ResizerControls, splitStyles } from '@cloudbeaver/core-blocks';
+import { TextareaNew } from '@cloudbeaver/core-blocks';
+import { composes, useStyles } from '@cloudbeaver/core-theming';
+
+import { ExecutionPlanTree } from './ExecutionPlanTree';
+
+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 {
+ query: string;
+ className?: string;
+}
+
+export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTreeBlock({ className, query }) {
+ const style = useStyles(styles, splitStyles);
+
+ 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..e08c9f73bd
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 {
+ selectedNodes: Map;
+ readonly columns: ObjectPropertyInfo[];
+ readonly nodes: IExecutionPlanNode[];
+ readonly selectedNode: SqlExecutionPlanNode | undefined;
+ selectNode: (nodeId: string) => void;
+}
+
+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..a4c5c2243a
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 { 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;
+ cursor: pointer;
+ }
+ 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 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 = property ? getPropertyValue(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/SQLExecutionPlanProcess.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SQLExecutionPlanProcess.ts
new file mode 100644
index 0000000000..14f853053b
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SQLExecutionPlanProcess.ts
@@ -0,0 +1,157 @@
+/*
+ * 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 { NotificationService } from '@cloudbeaver/core-events';
+import {
+ AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecutionPlan
+} from '@cloudbeaver/core-sdk';
+import {
+ CancellablePromise, cancellableTimeout, Deferred, EDeferredState
+} from '@cloudbeaver/core-utils';
+import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
+
+const DELAY_BETWEEN_TRIES = 1000;
+
+export class SQLExecutionPlanProcess extends Deferred {
+ private taskId?: string;
+ private timeout?: CancellablePromise;
+ private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
+
+ constructor(
+ private graphQLService: GraphQLService,
+ private notificationService: NotificationService
+ ) {
+ super();
+ }
+
+ async start(
+ query: string,
+ context: IDatabaseExecutionContext,
+ ): Promise {
+ // start async task
+ try {
+ const taskInfo = await this.explainExecutionPlanAsync(query, context);
+ await this.applyResult(taskInfo);
+ this.taskId = taskInfo.id;
+ if (this.getState() === EDeferredState.CANCELLING) {
+ await this.cancelAsync(this.taskId);
+ }
+ } catch (e) {
+ this.onError(e);
+ return;
+ }
+
+ if (this.isFinished) {
+ return;
+ }
+
+ // check async task status until execution finished
+ while (this.isInProgress) {
+ if (this.getState() === EDeferredState.CANCELLING) {
+ await this.cancelAsync(this.taskId);
+ }
+ // run the first check immediately because usually the query execution is fast
+ try {
+ const taskInfo = await this.getExecutionPlanStatusAsync(this.taskId);
+ await this.applyResult(taskInfo);
+ if (this.isFinished) {
+ return;
+ }
+ } catch (e) {
+ this.notificationService.logException(e, 'Failed to check async task status');
+ }
+
+ try {
+ this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
+ await this.timeout;
+ } catch {
+ }
+ }
+ }
+
+ /**
+ * this method just mark process as cancelling
+ * to avoid racing conditions the server request will be executed in sinchronious manner in start method
+ */
+ async cancel(): Promise {
+ if (this.getState() !== EDeferredState.PENDING) {
+ return;
+ }
+ this.toCancelling();
+ if (this.timeout) {
+ this.timeout.cancel();
+ }
+ }
+
+ private async cancelAsync(taskId: string) {
+ if (this.isCancelConfirmed) {
+ return;
+ }
+ try {
+ await this.cancelQueryAsync(taskId);
+ this.isCancelConfirmed = true;
+ } catch (e) {
+ if (this.getState() === EDeferredState.CANCELLING) {
+ this.toPending();
+ this.notificationService.logException(e, 'Failed to cancel async task');
+ }
+ }
+ }
+
+ private async explainExecutionPlanAsync(
+ query: string,
+ context: IDatabaseExecutionContext,
+ ): Promise {
+ const { taskInfo } = await this.graphQLService.sdk.asyncSqlExplainExecutionPlan({
+ connectionId: context.connectionId,
+ contextId: context.contextId,
+ query,
+ configuration: {},
+ });
+ return taskInfo;
+ }
+
+ private async getExecutionPlanStatusAsync(taskId: string): Promise {
+ const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId, removeOnFinish: false });
+ return taskInfo;
+ }
+
+ private async cancelQueryAsync(taskId: string): Promise {
+ await this.graphQLService.sdk.asyncTaskCancel({ taskId });
+ }
+
+ private async applyResult(taskInfo: AsyncTaskInfo) {
+ // task is running
+ if (taskInfo.running) {
+ return;
+ }
+ // task failed to execute
+ if (taskInfo.error) {
+ const serverError = new ServerInternalError(taskInfo.error);
+ this.onError(serverError, taskInfo.status);
+ return;
+ }
+ try {
+ // task execution successful
+ const { result } = await this.graphQLService.sdk.getSqlExecutionPlanResult({ taskId: taskInfo.id });
+ this.toResolved({ nodes: result.nodes, query: result.query });
+ await this.graphQLService.sdk.getAsyncTaskInfo({ taskId: taskInfo.id, removeOnFinish: true });
+ } catch (exception) {
+ this.onError(new Error('Tasks execution returns no result'), exception);
+ }
+ }
+
+ private onError(error: Error, status?: string) {
+ // if task failed to execute during cancelling - it means it was cancelled successfully
+ if (this.getState() === EDeferredState.CANCELLING) {
+ this.toCancelled(error);
+ } else {
+ this.toRejected(error);
+ }
+ }
+}
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..f5e2a20247
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 tabId = executionPlanTab.tabId;
+ const sqlExecutionPlanService = useService(SqlExecutionPlanService);
+ const executionPlan = sqlExecutionPlanService.results.get(tabId);
+ const process = sqlExecutionPlanService.processes.get(tabId);
+
+ const executionPlanState = useExecutionPlanTreeState(executionPlan?.nodes || []);
+ const loading = process?.isInProgress || !executionPlan;
+ const canCancel = 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.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..00ba9ab91e
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts
@@ -0,0 +1,141 @@
+/*
+ * 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 } 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';
+
+@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, query);
+ const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query);
+ const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
+
+ try {
+ this.processes.set(tabId, task);
+ executionState.setExecutionTask(task);
+ editorState.currentTabId = tabId;
+
+ await task.start(
+ subQuery,
+ {
+ connectionId: editorState.executionContext.connectionId,
+ contextId: editorState.executionContext.contextId,
+ }
+ );
+
+ const executionPlan = await task.promise;
+ this.results.set(tabId, executionPlan);
+ } 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) {
+ const tab = state.tabs.find(tab => tab.id === tabId);
+ if (tab) {
+ state.tabs.splice(state.tabs.indexOf(tab), 1);
+ }
+ 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 executionPlanTab = state.executionPlanTabs.find(executionPlanTab => executionPlanTab.tabId === tabId);
+
+ if (executionPlanTab) {
+ state.executionPlanTabs.splice(state.executionPlanTabs.indexOf(executionPlanTab), 1);
+ }
+
+ this.results.delete(tabId);
+ this.processes.delete(tabId);
+ }
+
+ private createExecutionPlanTab(
+ state: ISqlEditorTabState,
+ query: string,
+ ) {
+ if (!state.executionContext) {
+ throw new Error('ExecutionContext is not provided');
+ }
+
+ const id = uuid();
+ const order = Math.max(0, ...state.tabs.map(tab => tab.order + 1));
+ const nameOrder = Math.max(1, ...state.executionPlanTabs.map(tab => tab.order + 1));
+
+ state.executionPlanTabs.push({
+ tabId: id,
+ executionContext: {
+ connectionId: state.executionContext.connectionId,
+ contextId: state.executionContext.contextId,
+ },
+ query,
+ order: nameOrder,
+ });
+
+ state.tabs.push({
+ id,
+ name: `Execution plan - ${nameOrder}`,
+ 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..c054310fd3
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { useObjectRef } from '@cloudbeaver/core-blocks';
+import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
+
+import type { IExecutionPlanNode, IExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
+
+export function isVisibleProperty(property: ObjectPropertyInfo): boolean {
+ return property.features.includes('viewable');
+}
+
+interface IPrivateExecutionPlanTreeState extends IExecutionPlanTreeContext {
+ list: SqlExecutionPlanNode[];
+}
+
+export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecutionPlanTreeContext {
+ return useObjectRef({
+ list,
+ selectedNodes: new Map(),
+ get columns() {
+ const columns: ObjectPropertyInfo[] = [];
+
+ for (const node of this.list) {
+ for (const property of node.properties) {
+ if (property.id && isVisibleProperty(property) && !columns.find(column => column.id === property.id)) {
+ columns.push(property);
+ }
+ }
+ }
+
+ return columns;
+ },
+ get nodes() {
+ const map: Map = new Map();
+
+ const tree: IExecutionPlanNode[] = this.list
+ .map((node, idx) => {
+ map.set(node.id, idx);
+ return ({ ...node, children: [] });
+ });
+
+ const nodes: IExecutionPlanNode[] = [];
+
+ for (const node of tree) {
+ if (node.parentId) {
+ const parent = map.get(node.parentId)!;
+ tree[parent].children.push(node);
+ } else {
+ nodes.push(node);
+ }
+ }
+
+ return nodes;
+ },
+ get selectedNode(): SqlExecutionPlanNode | undefined {
+ const nodeId = Array.from(this.selectedNodes.keys())[0];
+ return this.list.find(node => node.id === nodeId);
+ },
+ selectNode(nodeId: string) {
+ this.selectedNodes.clear();
+ this.selectedNodes.set(nodeId, true);
+ },
+ }, { list }, {
+ selectedNodes: observable,
+ list: observable,
+ columns: computed,
+ nodes: computed,
+ selectedNode: computed,
+ }, ['selectNode']);
+}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts
index 2550cb9f5c..7bfdd3058c 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts
@@ -131,6 +131,7 @@ export class SqlQueryService {
tabState: ISqlEditorTabState,
modelId: string
): IResultGroup {
+ const nameOrder = Math.max(1, ...tabState.resultGroups.map(group => group.nameOrder + 1));
const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1));
const groupId = uuid();
@@ -138,6 +139,7 @@ export class SqlQueryService {
groupId,
modelId,
order,
+ nameOrder,
sqlQueryParams: params,
});
@@ -208,7 +210,7 @@ export class SqlQueryService {
const tab = state.tabs.find(tab => tab.id === resultTab.tabId);
if (tab) {
- tab.name = this.getTabNameForOrder(group.order, indexInResultSet, model.source.results.length);
+ tab.name = this.getTabNameForOrder(group.nameOrder, indexInResultSet, model.source.results.length);
}
}
}
@@ -229,13 +231,13 @@ export class SqlQueryService {
state.tabs.push({
id,
- name: this.getTabNameForOrder(group.order, indexInResultSet, results),
+ name: this.getTabNameForOrder(group.nameOrder, indexInResultSet, results),
icon: '/icons/grid.png',
order: group.order,
});
}
private getTabNameForOrder(order: number, indexInResultSet: number, results: number) {
- return `Result - ${order + 1}` + (results > 1 ? ` (${indexInResultSet + 1})` : '');
+ return `Result - ${order}` + (results > 1 ? ` (${indexInResultSet + 1})` : '');
}
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx
index a720ed0b00..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)(
- {/* TODO: ex-plan add panel */}
+
);
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx
index 0638adf8c8..4f2d200f44 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%;
@@ -70,12 +74,12 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
})
),
[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 +89,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
(
+ tabs={orderedTabs.map(result => (
{result.name}
@@ -93,7 +97,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
))}
style={[styles]}
>
- {orderedTabs.get().map(result => (
+ {orderedTabs.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..18b7eb420d 100644
--- a/webapp/packages/plugin-sql-editor/src/locales/en.ts
+++ b/webapp/packages/plugin-sql-editor/src/locales/en.ts
@@ -1,3 +1,10 @@
export default [
['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'],
+ ['sql_editor_execution_plan_button_tooltip', 'Explain execution plan (Shift + Ctrl + E)'],
+ ['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..9d5ae60a9b 100644
--- a/webapp/packages/plugin-sql-editor/src/locales/ru.ts
+++ b/webapp/packages/plugin-sql-editor/src/locales/ru.ts
@@ -1,3 +1,10 @@
export default [
['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'],
+ ['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'],
+ ['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 21075adbaf..afbc9e5f61 100644
--- a/webapp/packages/plugin-sql-editor/src/manifest.ts
+++ b/webapp/packages/plugin-sql-editor/src/manifest.ts
@@ -14,6 +14,7 @@ import { SqlEditorBootstrap } from './SqlEditorBootstrap';
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
import { SqlEditorService } from './SqlEditorService';
import { SqlEditorTabService } from './SqlEditorTabService';
+import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from './SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';
@@ -27,6 +28,7 @@ export const sqlEditorPluginManifest: PluginManifest = {
SqlDialectInfoService,
SqlEditorTabService,
SqlQueryService,
+ SqlExecutionPlanService,
SqlResultTabsService,
SqlEditorService,
SqlEditorNavigatorService,