From e26346d055710a8da0fb72353dc532dd97bcb3e4 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 16 Jun 2021 17:35:45 +0300 Subject: [PATCH 1/7] feat(plugin-sql-editor): CB-1060 asyncSqlExplainExecutionPlan test --- .../grid/asyncSqlExplainExecutionPlan.gql | 24 +++ .../grid/getSqlExecutionPlanResult.gql | 28 +++ .../src/queries/querySqlDialectInfo.gql | 1 + webapp/packages/core-sdk/src/sdk.ts | 164 ++++++++++++++++-- .../src/SqlEditor/SqlEditor.tsx | 11 +- .../src/SqlEditor/SqlEditorController.tsx | 12 +- .../SqlResultTabs/SQLExecutionPlanProcess.ts | 157 +++++++++++++++++ .../SqlResultTabs/SqlExecutionPlanService.ts | 108 ++++++++++++ .../src/SqlResultTabs/SqlResultPanel.tsx | 2 +- .../plugin-sql-editor/src/manifest.ts | 2 + 10 files changed, 491 insertions(+), 18 deletions(-) create mode 100644 webapp/packages/core-sdk/src/queries/grid/asyncSqlExplainExecutionPlan.gql create mode 100644 webapp/packages/core-sdk/src/queries/grid/getSqlExecutionPlanResult.gql create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLExecutionPlanProcess.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts 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/queries/querySqlDialectInfo.gql b/webapp/packages/core-sdk/src/queries/querySqlDialectInfo.gql index 4c5dfc4be5..33d8e7c16b 100644 --- a/webapp/packages/core-sdk/src/queries/querySqlDialectInfo.gql +++ b/webapp/packages/core-sdk/src/queries/querySqlDialectInfo.gql @@ -12,5 +12,6 @@ query querySqlDialectInfo( $connectionId: ID! ) { catalogSeparator structSeparator scriptDelimiter + supportsExplainExecutionPlan } } diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index d55e29eca1..5bfd935454 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -283,6 +283,8 @@ export interface Mutation { asyncReadDataFromContainer: AsyncTaskInfo; asyncSqlExecuteQuery: AsyncTaskInfo; asyncSqlExecuteResults: SqlExecuteInfo; + asyncSqlExplainExecutionPlan: AsyncTaskInfo; + asyncSqlExplainExecutionPlanResult: SqlExecutionPlan; asyncTaskCancel?: Maybe; asyncTaskInfo: AsyncTaskInfo; /** @deprecated Field no longer supported */ @@ -332,6 +334,17 @@ export interface MutationAsyncSqlExecuteResultsArgs { taskId: Scalars['ID']; } +export interface MutationAsyncSqlExplainExecutionPlanArgs { + connectionId: Scalars['ID']; + contextId: Scalars['ID']; + query: Scalars['String']; + configuration: Scalars['Object']; +} + +export interface MutationAsyncSqlExplainExecutionPlanResultArgs { + taskId: Scalars['ID']; +} + export interface MutationAsyncTaskCancelArgs { id: Scalars['String']; } @@ -772,26 +785,27 @@ export interface DatabaseStructContainers { } export interface SqlDialectInfo { - name?: Maybe; - dataTypes?: Maybe>>; - functions?: Maybe>>; - reservedWords?: Maybe>>; - quoteStrings?: Maybe>>>>; - singleLineComments?: Maybe>>; - multiLineComments?: Maybe>>>>; + name: Scalars['String']; + dataTypes: Array>; + functions: Array>; + reservedWords: Array>; + quoteStrings: Array>>>; + singleLineComments: Array>; + multiLineComments: Array>>>; catalogSeparator?: Maybe; structSeparator?: Maybe; scriptDelimiter?: Maybe; + supportsExplainExecutionPlan: Scalars['Boolean']; } export interface SqlCompletionProposal { - displayString?: Maybe; - type?: Maybe; + displayString: Scalars['String']; + type: Scalars['String']; score?: Maybe; - replacementString?: Maybe; - replacementOffset?: Maybe; - replacementLength?: Maybe; - cursorPosition?: Maybe; + replacementString: Scalars['String']; + replacementOffset: Scalars['Int']; + replacementLength: Scalars['Int']; + cursorPosition: Scalars['Int']; icon?: Maybe; nodePath?: Maybe; } @@ -876,6 +890,22 @@ export interface DataTypeLogicalOperation { argumentCount?: Maybe; } +export interface SqlExecutionPlan { + query: Scalars['String']; + nodes: SqlExecutionPlanNode[]; +} + +export interface SqlExecutionPlanNode { + id: Scalars['ID']; + parentId?: Maybe; + kind: Scalars['String']; + name?: Maybe; + type: Scalars['String']; + condition?: Maybe; + description?: Maybe; + properties: ObjectPropertyInfo[]; +} + export enum AdminSubjectType { User = 'user', Role = 'role' @@ -1399,6 +1429,11 @@ export type ObjectOriginInfoFragment = ( & { details?: Maybe>> } ); +export type SqlExecutionPlanNodeFragment = ( + Pick + & { properties: Array> } +); + export type SessionStateFragment = Pick; export type UserConnectionAuthPropertiesFragment = Pick; @@ -1456,6 +1491,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']; }>; @@ -1476,6 +1525,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']; @@ -1580,7 +1643,7 @@ export type QuerySqlDialectInfoQueryVariables = Exact<{ connectionId: Scalars['ID']; }>; -export interface QuerySqlDialectInfoQuery { dialect?: Maybe> } +export interface QuerySqlDialectInfoQuery { dialect?: Maybe> } export type ConfigureServerQueryVariables = Exact<{ configuration: ServerConfigInput; @@ -1842,6 +1905,25 @@ 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 @@ -2351,6 +2433,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) { @@ -2391,6 +2494,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( @@ -2520,6 +2649,7 @@ export const QuerySqlDialectInfoDocument = ` catalogSeparator structSeparator scriptDelimiter + supportsExplainExecutionPlan } } `; @@ -2799,9 +2929,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/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index 2e3d8469ac..bb59067aff 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -79,8 +79,8 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd } return styled(styles)( - - + + + => { + this.sqlExecutionPlanService.executeExecutionPlan( + this.sqlResultTabsService.getTabExecutionContext(this.tab.id), + this.tab.handlerState, + await this.getExecutingQuery(), + ); + }; + // TODO: ex-plan add action for execution plan readonly options: EditorConfiguration = { @@ -96,7 +105,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/SqlResultTabs/SQLExecutionPlanProcess.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLExecutionPlanProcess.ts new file mode 100644 index 0000000000..49184c45b6 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/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, SqlExecuteInfo, SqlDataFilter, ResultDataFormat, 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.executeQueryAsync(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.getQueryStatusAsync(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 executeQueryAsync( + query: string, + context: IDatabaseExecutionContext, + ): Promise { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlExplainExecutionPlan({ + connectionId: context.connectionId, + contextId: context.contextId, + query, + configuration: {}, + }); + return taskInfo; + } + + private async getQueryStatusAsync(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/SqlExecutionPlanService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts new file mode 100644 index 0000000000..6043f5e1a1 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlExecutionPlanService.ts @@ -0,0 +1,108 @@ +/* + * 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/SqlResultPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx index a720ed0b00..d43d63ed6e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx @@ -44,7 +44,7 @@ export const SqlResultPanel = observer(function SqlResultPanel({ tab, id }: SqlR if (executionPlanTab) { return styled(style)( - {/* TODO: ex-plan add panel */} +
{executionPlanTab.query}
); } diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index 21075adbaf..01483533fe 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/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, From dff98db98f9b46521cbd9eae89eb0365eb45ccd0 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 10:30:17 +0300 Subject: [PATCH 2/7] 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'; From 5f2e0f4b9f3fe6f50c675e4b43a676d330faa536 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 10:40:23 +0300 Subject: [PATCH 3/7] feat(plugin-sql-editor): CB-1060 execution plan icon --- webapp/packages/core-app/public/icons/icons.svg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index bcefbfc39d..e70ea3855a 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -192,4 +192,18 @@ + + + + + + + + + + + + + + From d45f68deaaac4a31e7cf41e91378f134e98dbe73 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 10:52:32 +0300 Subject: [PATCH 4/7] feat(plugin-sql-editor): CB-1060 remove unused dependencies --- .../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts index 5c4e754343..4415c4a348 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { makeObservable, observable, toJS } from 'mobx'; +import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -119,7 +119,7 @@ export class SqlExecutionPlanService { state: ISqlEditorTabState, ) { if (!state.executionContext) { - throw new Error('executeEditorQuery executionContext is not provided'); + throw new Error('ExecutionContext is not provided'); } const id = uuid(); From 15241376b655b1b0628625bf07c8f94fdb910d0b Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 17:56:48 +0300 Subject: [PATCH 5/7] refactor(plugin-sql-editor): CB-1060 tabs behaviour cahnges --- .../src/ISqlEditorTabState.ts | 2 + .../src/SqlEditor/SqlEditor.tsx | 19 ++-- .../ExecutionPlan/ExecutionPlanTree.tsx | 15 +-- .../ExecutionPlan/ExecutionPlanTreeBlock.tsx | 5 +- .../ExecutionPlan/ExecutionPlanTreeContext.ts | 11 +- .../ExecutionPlan/NestedNode.tsx | 3 +- .../ExecutionPlan/QueryPanel.tsx | 5 +- .../ExecutionPlan/SqlExecutionPlanPanel.tsx | 17 +-- .../ExecutionPlan/SqlExecutionPlanService.ts | 43 ++++---- .../useExecutionPlanTreeState.ts | 103 ++++++++---------- .../src/SqlResultTabs/SqlQueryService.ts | 14 +-- .../src/SqlResultTabs/SqlResultTabs.tsx | 21 +--- .../plugin-sql-editor/src/locales/en.ts | 1 + .../plugin-sql-editor/src/locales/ru.ts | 5 +- 14 files changed, 115 insertions(+), 149 deletions(-) diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index 7c47ad5d0a..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 { @@ -38,6 +39,7 @@ export interface ISqlEditorResultTab { export interface IExecutionPlanTab { tabId: string; executionContext: IDatabaseExecutionContext; + query: string; order: number; options?: Record; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index 15fc0c3530..c4f8b54a55 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -13,6 +13,7 @@ import styled, { css } from 'reshadow'; import { useTab } from '@cloudbeaver/core-app'; 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,23 +45,20 @@ const styles = css` background: none; } - StaticImage, icon-container { + StaticImage, Icon { padding: 4px; height: 24px; width: 24px; cursor: pointer; } - icon-container { - display: flex; - } - SQLCodeEditorLoader { flex: 1; } `; export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEditorProps) { + const translate = useTranslate(); const tab = useTab(tabId); const editor = useRef(null); const baseTab = useBaseTab(tabId); @@ -107,15 +105,14 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd = 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 => ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx index b2360c0368..066cbb7baf 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx @@ -38,10 +38,11 @@ const styles = composes( ); interface Props { + query: string; className?: string; } -export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTreeBlock({ className }) { +export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTreeBlock({ className, query }) { const style = useStyles(styles, splitStyles); const treeContext = useContext(ExecutionPlanTreeContext); @@ -56,7 +57,7 @@ export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTre - + ); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts index 2279ecb5f8..e08c9f73bd 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeContext.ts @@ -15,12 +15,11 @@ export interface IExecutionPlanNode extends SqlExecutionPlanNode { } export interface IExecutionPlanTreeContext { - query: string; - columns: ObjectPropertyInfo[]; - nodes: IExecutionPlanNode[]; - selectNode: (node: IExecutionPlanNode) => void; - selectedNode: IExecutionPlanNode | null; - metadataPanel: boolean; + 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/NestedNode.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx index 3eae668539..3ffe93bb53 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx @@ -20,6 +20,7 @@ import { getPropertyValue } from './getPropertyValue'; const styles = css` TableColumnValue { white-space: pre; + cursor: pointer; } expand-container { width: 24px; @@ -65,7 +66,7 @@ export const NestedNode: React.FC = function NestedNode({ node, depth, cl return styled(style)( <> - + {treeContext.columns.map((column, idx) => { const property = node.properties.find(property => property.id === column.id); const value = getValue(property); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx index 4bcc0e798f..ba4b160616 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx @@ -13,10 +13,11 @@ import { TextareaNew } from '@cloudbeaver/core-blocks'; import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; interface Props { + query: string; className?: string; } -export const QueryPanel: React.FC = function QueryPanel({ className }) { +export const QueryPanel: React.FC = function QueryPanel({ className, query }) { const treeContext = useContext(ExecutionPlanTreeContext); if (!treeContext) { @@ -28,7 +29,7 @@ export const QueryPanel: React.FC = function QueryPanel({ className }) { className={className} name='value' rows={3} - value={treeContext.query} + value={query} readOnly embedded /> diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx index a2ba06e80c..f5e2a20247 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanPanel.tsx @@ -46,13 +46,14 @@ export const SqlExecutionPlanPanel: React.FC = observer(function SqlExecu }) { const style = useStyles(styles, splitStyles); const translate = useTranslate(); + const tabId = executionPlanTab.tabId; const sqlExecutionPlanService = useService(SqlExecutionPlanService); - const data = sqlExecutionPlanService.results.get(executionPlanTab.tabId); - const process = sqlExecutionPlanService.processes.get(executionPlanTab.tabId); + const executionPlan = sqlExecutionPlanService.results.get(tabId); + const process = sqlExecutionPlanService.processes.get(tabId); - const executionPlanState = useExecutionPlanTreeState(data?.executionPlan.nodes || [], data?.executionPlan.query || ''); - const loading = process?.isInProgress || !data; - const canCancel = process ? process?.getState() === EDeferredState.PENDING : false; + const executionPlanState = useExecutionPlanTreeState(executionPlan?.nodes || []); + const loading = process?.isInProgress || !executionPlan; + const canCancel = process?.getState() === EDeferredState.PENDING ?? false; const cancelTask = useCallback(() => { if (process) { @@ -75,11 +76,11 @@ export const SqlExecutionPlanPanel: React.FC = observer(function SqlExecu return styled(style)( - + - + - {executionPlanState.metadataPanel && executionPlanState.selectedNode && ( + {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 index 4415c4a348..00ba9ab91e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.ts @@ -18,13 +18,9 @@ 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; + results: Map; processes: Map; constructor( @@ -50,28 +46,25 @@ export class SqlExecutionPlanService { return; } - const tabId = this.createExecutionPlanTab(editorState); + const tabId = this.createExecutionPlanTab(editorState, query); 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 { + this.processes.set(tabId, task); + executionState.setExecutionTask(task); + editorState.currentTabId = tabId; + await task.start( subQuery, { - connectionId: editorState.executionContext?.connectionId, - contextId: editorState.executionContext?.contextId, + connectionId: editorState.executionContext.connectionId, + contextId: editorState.executionContext.contextId, } ); const executionPlan = await task.promise; - this.results.set(tabId, { - executionPlan, - query, - }); + 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); @@ -90,6 +83,10 @@ export class SqlExecutionPlanService { } 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) { @@ -100,30 +97,27 @@ export class SqlExecutionPlanService { } 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, + query: string, ) { if (!state.executionContext) { throw new Error('ExecutionContext is not provided'); } const id = uuid(); - const order = Math.max(0, ...state.executionPlanTabs.map(tab => tab.order + 1)); + 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, @@ -131,12 +125,13 @@ export class SqlExecutionPlanService { connectionId: state.executionContext.connectionId, contextId: state.executionContext.contextId, }, - order, + query, + order: nameOrder, }); state.tabs.push({ id, - name: `Execution plan - ${order + 1}`, + name: `Execution plan - ${nameOrder}`, icon: 'execution-plan-tab', order, }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts index 4de07a8b4e..c054310fd3 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/useExecutionPlanTreeState.ts @@ -7,79 +7,72 @@ */ 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, - })); +interface IPrivateExecutionPlanTreeState extends IExecutionPlanTreeContext { + list: SqlExecutionPlanNode[]; +} - const selectNode = useCallback((node: IExecutionPlanNode) => { - if (!state.metadataPanel) { - state.metadataPanel = true; - } - state.selectedNode = node; - }, [state]); +export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecutionPlanTreeContext { + return useObjectRef({ + list, + selectedNodes: new Map(), + get columns() { + const columns: ObjectPropertyInfo[] = []; - 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); + 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 result; - }), [props.list]).get(); + return columns; + }, + get nodes() { + const map: Map = new Map(); - const nodes = useMemo(() => computed(() => { - const map: Map = new Map(); + const tree: IExecutionPlanNode[] = this.list + .map((node, idx) => { + map.set(node.id, idx); + return ({ ...node, children: [] }); + }); - const tree: IExecutionPlanNode[] = props.list - .map((node, idx) => { - map.set(node.id, idx); - return ({ ...node, children: [] }); - }); + const nodes: IExecutionPlanNode[] = []; - 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); + for (const node of tree) { + if (node.parentId) { + const parent = map.get(node.parentId)!; + tree[parent].children.push(node); + } else { + nodes.push(node); + } } - } - return result; - }), [props.list]).get(); - - return useObjectRef({ - columns, - nodes, - metadataPanel: state.metadataPanel, - selectedNode: state.selectedNode, - query: props.query, - selectNode, - }); + 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 4559899a1e..7bfdd3058c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts @@ -131,17 +131,15 @@ export class SqlQueryService { tabState: ISqlEditorTabState, modelId: string ): IResultGroup { - 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 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(); tabState.resultGroups.push({ groupId, modelId, order, + nameOrder, sqlQueryParams: params, }); @@ -212,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); } } } @@ -233,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/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index f0348ef372..4f2d200f44 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -74,22 +74,7 @@ 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(); + ).get(); const handleOpen = ({ tabId }: ITabData) => navigatorService.openEditorResult(tab.id, tabId); const handleClose = ({ tabId }: ITabData) => navigatorService.closeEditorResult(tab.id, tabId); @@ -104,7 +89,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes ( + tabs={orderedTabs.map(result => ( {result.name} @@ -112,7 +97,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes ))} style={[styles]} > - {categorizedTabs.map(result => ( + {orderedTabs.map(result => ( diff --git a/webapp/packages/plugin-sql-editor/src/locales/en.ts b/webapp/packages/plugin-sql-editor/src/locales/en.ts index d5f3865d62..18b7eb420d 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/en.ts @@ -1,5 +1,6 @@ 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'], diff --git a/webapp/packages/plugin-sql-editor/src/locales/ru.ts b/webapp/packages/plugin-sql-editor/src/locales/ru.ts index c18f1ac829..9d5ae60a9b 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/ru.ts @@ -1,9 +1,10 @@ export default [ ['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'], - ['sql_execution_plan_placeholder', 'План выполнения пустой'], + ['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', 'Детали'], + ['sql_execution_plan_properties_panel_details', 'Дополнительные'], ]; From fbee8ee91ab33a7e28eb8a4e10e8208eb7929cb0 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 18:02:21 +0300 Subject: [PATCH 6/7] refactor(plugin-sql-editor): CB-1060 remove unnecessary context usage --- .../SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx index 066cbb7baf..b8d910864b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx @@ -44,11 +44,6 @@ interface Props { export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTreeBlock({ className, query }) { const style = useStyles(styles, splitStyles); - const treeContext = useContext(ExecutionPlanTreeContext); - - if (!treeContext) { - throw new Error('Tree context must be provided'); - } return styled(style)( From 031ea13fe8948c152b33f75d148c263cbccb18e7 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 24 Jun 2021 18:37:48 +0300 Subject: [PATCH 7/7] refactor(plugin-sql-editor): CB-1060 remove unnecessary context usage --- .../ExecutionPlan/ExecutionPlanTreeBlock.tsx | 13 +++++-- .../ExecutionPlan/NestedNode.tsx | 11 +----- .../ExecutionPlan/QueryPanel.tsx | 37 ------------------- 3 files changed, 10 insertions(+), 51 deletions(-) delete mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx index b8d910864b..281754417c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx @@ -6,15 +6,13 @@ * 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 { TextareaNew } 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` @@ -52,7 +50,14 @@ export const ExecutionPlanTreeBlock: React.FC = function ExecutionPlanTre - + ); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx index 3ffe93bb53..a4c5c2243a 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/NestedNode.tsx @@ -10,7 +10,6 @@ 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'; @@ -49,14 +48,6 @@ export const NestedNode: React.FC = function NestedNode({ node, depth, cl 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); @@ -69,7 +60,7 @@ export const NestedNode: React.FC = function NestedNode({ node, depth, cl {treeContext.columns.map((column, idx) => { const property = node.properties.find(property => property.id === column.id); - const value = getValue(property); + const value = property ? getPropertyValue(property) : ''; return ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx deleted file mode 100644 index ba4b160616..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/QueryPanel.tsx +++ /dev/null @@ -1,37 +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 { useContext } from 'react'; - -import { TextareaNew } from '@cloudbeaver/core-blocks'; - -import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext'; - -interface Props { - query: string; - className?: string; -} - -export const QueryPanel: React.FC = function QueryPanel({ className, query }) { - const treeContext = useContext(ExecutionPlanTreeContext); - - if (!treeContext) { - throw new Error('Tree context must be provided'); - } - - return ( - - ); -};