mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #395 from dbeaver/feat/execution-plan
Feat/execution plan
This commit is contained in:
@@ -183,7 +183,7 @@
|
||||
<symbol id="order-arrow-down" viewBox="0 0 16 16">
|
||||
<path fill="#338ECC" fill-rule="evenodd" d="M15 9l-7 6-7-6h5V1h4v8h5z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="order-arrow-unknown" viewBox="0 0 16 16">
|
||||
<symbol id="order-arrow-unknown" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M11.5 2L8 6h2.333v8h2.334V6H15l-3.5-4zm-7 12L8 10H5.667V2H3.333v8H1l3.5 4z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="password-hide" viewBox="0 0 16 16">
|
||||
@@ -192,4 +192,18 @@
|
||||
<symbol id="password-show" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 11.25C10.8247 11.25 12.6094 9.5143 13.5609 8C12.6094 6.4857 10.8247 4.75 8 4.75C5.17528 4.75 3.39058 6.4857 2.43914 8C3.39058 9.5143 5.17528 11.25 8 11.25ZM15 8C14.0667 9.8 11.866 12.5 8 12.5C4.13401 12.5 1.93333 9.8 1 8C1.93333 6.2 4.13401 3.5 8 3.5C11.866 3.5 14.0667 6.2 15 8ZM7.99997 9.88333C9.0309 9.88333 9.86663 9.0476 9.86663 8.01666C9.86663 6.98573 9.0309 6.15 7.99997 6.15C6.96904 6.15 6.1333 6.98573 6.1333 8.01666C6.1333 9.0476 6.96904 9.88333 7.99997 9.88333Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="execution-plan" viewBox="0 0 32 32">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31 2.5C31 1.11929 29.8807 0 28.5 0H8.39735C6.74049 0 5.39735 1.34315 5.39735 3L5.39734 5L5.4 4.99684V23H7.6V4.5V4.49997C7.59999 3.9996 7.63686 3.49608 7.82593 3.13249C7.90761 2.97541 8.0105 2.86064 8.15272 2.77735C8.29873 2.69185 8.55425 2.6 9 2.6H9.01415H9.02847H9.04296H9.05762H9.07246H9.08746H9.10263H9.11797H9.13348H9.14915H9.16499H9.181H9.19717H9.21351H9.23001H9.24668H9.26351H9.2805H9.29766H9.31497H9.33245H9.35009H9.36788H9.38584H9.40396H9.42223H9.44066H9.45925H9.47799H9.49689H9.51595H9.53516H9.55452H9.57404H9.59371H9.61353H9.63351H9.65363H9.67391H9.69434H9.71491H9.73564H9.75651H9.77753H9.7987H9.82002H9.84148H9.86309H9.88484H9.90673H9.92877H9.95096H9.97328H9.99575H10.0184H10.0411H10.064H10.087H10.1102H10.1335H10.157H10.1805H10.2043H10.2281H10.2521H10.2762H10.3005H10.3249H10.3494H10.3741H10.3989H10.4238H10.4489H10.4741H10.4994H10.5248H10.5504H10.5761H10.602H10.6279H10.654H10.6802H10.7066H10.733H10.7596H10.7863H10.8132H10.8401H10.8672H10.8944H10.9217H10.9492H10.9768H11.0044H11.0322H11.0602H11.0882H11.1163H11.1446H11.173H11.2015H11.2301H11.2589H11.2877H11.3167H11.3457H11.3749H11.4042H11.4336H11.4631H11.4927H11.5225H11.5523H11.5822H11.6123H11.6425H11.6727H11.7031H11.7336H11.7642H11.7948H11.8256H11.8565H11.8875H11.9186H11.9498H11.9811H12.0125H12.044H12.0756H12.1073H12.1391H12.171H12.2029H12.235H12.2672H12.2995H12.3318H12.3643H12.3968H12.4295H12.4622H12.495H12.5279H12.5609H12.594H12.6272H12.6605H12.6938H12.7273H12.7608H12.7944H12.8281H12.8619H12.8958H12.9298H12.9638H12.9979H13.0321H13.0664H13.1008H13.1352H13.1698H13.2044H13.239H13.2738H13.3086H13.3436H13.3786H13.4136H13.4488H13.484H13.5193H13.5546H13.5901H13.6256H13.6612H13.6968H13.7326H13.7683H13.8042H13.8401H13.8761H13.9122H13.9483H13.9846H14.0208H14.0572H14.0936H14.13H14.1665H14.2031H14.2398H14.2765H14.3133H14.3501H14.387H14.424H14.461H14.4981H14.5352H14.5724H14.6096H14.6469H14.6843H14.7217H14.7592H14.7967H14.8343H14.8719H14.9096H14.9473H14.9851H15.0229H15.0608H15.0987H15.1367H15.1747H15.2128H15.251H15.2891H15.3273H15.3656H15.4039H15.4423H15.4807H15.5191H15.5576H15.5961H15.6347H15.6733H15.7119H15.7506H15.7893H15.8281H15.8669H15.9057H15.9446H15.9835H16.0225H16.0615H16.1005H16.1395H16.1786H16.2177H16.2569H16.2961H16.3353H16.3745H16.4138H16.4531H16.4924H16.5318H16.5712H16.6106H16.65H16.6895H16.729H16.7685H16.808H16.8476H16.8872H16.9268H16.9664H17.0061H17.0457H17.0854H17.1251H17.1649H17.2046H17.2444H17.2842H17.324H17.3638H17.4036H17.4435H17.4834H17.5232H17.5631H17.603H17.643H17.6829H17.7228H17.7628H17.8027H17.8427H17.8827H17.9227H17.9627H18.0027H18.0427H18.0827H18.1227H18.1627H18.2028H18.2428H18.2828H18.3229H18.3629H18.4029H18.443H18.483H18.5231H18.5631H18.6031H18.6432H18.6832H18.7232H18.7633H18.8033H18.8433H18.8833H18.9233H18.9633H19.0033H19.0433H19.0833H19.1232H19.1632H19.2031H19.2431H19.283H19.3229H19.3628H19.4027H19.4426H19.4824H19.5223H19.5621H19.6019H19.6417H19.6815H19.7213H19.761H19.8007H19.8404H19.8801H19.9198H19.9594H19.9991H20.0387H20.0782H20.1178H20.1573H20.1968H20.2363H20.2758H20.3152H20.3546H20.394H20.4333H20.4727H20.512H20.5512H20.5905H20.6297H20.6688H20.708H20.7471H20.7861H20.8252H20.8642H20.9032H20.9421H20.981H21.0199H21.0587H21.0975H21.1362H21.1749H21.2136H21.2522H21.2908H21.3294H21.3679H21.4064H21.4448H21.4832H21.5215H21.5598H21.598H21.6362H21.6744H21.7125H21.7506H21.7886H21.8266H21.8645H21.9023H21.9402H21.9779H22.0156H22.0533H22.0909H22.1285H22.166H22.2034H22.2408H22.2782H22.3154H22.3527H22.3898H22.427H22.464H22.501H22.5379H22.5748H22.6116H22.6484H22.6851H22.7217H22.7583H22.7948H22.8312H22.8676H22.9L22.9 28V28C22.9 28.5039 22.8677 29.0333 22.6921 29.4181C22.6146 29.5879 22.5253 29.6909 22.4266 29.7567C22.3313 29.8202 22.1516 29.9 21.8049 29.9H20V26H0V32H6V32.1H14.6488H21.8049C22.5118 32.1 23.1333 31.9298 23.6472 31.587C24.1577 31.2466 24.4855 30.7871 24.6933 30.3319C25.0884 29.4667 25.1 28.4962 25.1 28V28L25.1 5H31V2.5ZM10 7H14V9H10V7ZM14 13H10V15H14V13ZM10 19H14V21H10V19ZM20 7H16V9H20V7ZM16 13H20V15H16V13ZM20 19H16V21H20V19Z" fill="#338ECC"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</symbol>
|
||||
<symbol id="execution-plan-tab" viewBox="0 0 32 32">
|
||||
<path d="M5 2H19.5858L27 9.41421V30H5V2Z" stroke="#338ECC" stroke-width="2" fill="transparent"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 1L28 10H19V1ZM8 8H13V12H8V8ZM24 22H19V26H24V22ZM19 15H24V19H19V15ZM11.5 13H9.5V25H10H11.5H18V23H11.5V18H18V16H11.5V13Z" fill="#338ECC"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 25 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -806,7 +806,7 @@ export interface SqlCompletionProposal {
|
||||
replacementString: Scalars['String'];
|
||||
replacementOffset: Scalars['Int'];
|
||||
replacementLength: Scalars['Int'];
|
||||
cursorPosition: Scalars['Int'];
|
||||
cursorPosition?: Maybe<Scalars['Int']>;
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
nodePath?: Maybe<Scalars['String']>;
|
||||
}
|
||||
@@ -1488,6 +1488,20 @@ export interface AsyncSqlExecuteQueryMutation {
|
||||
);
|
||||
}
|
||||
|
||||
export type AsyncSqlExplainExecutionPlanMutationVariables = Exact<{
|
||||
connectionId: Scalars['ID'];
|
||||
contextId: Scalars['ID'];
|
||||
query: Scalars['String'];
|
||||
configuration: Scalars['Object'];
|
||||
}>;
|
||||
|
||||
export interface AsyncSqlExplainExecutionPlanMutation {
|
||||
taskInfo: (
|
||||
Pick<AsyncTaskInfo, 'id' | 'name' | 'running' | 'status' | 'taskResult'>
|
||||
& { error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
|
||||
);
|
||||
}
|
||||
|
||||
export type GetSqlExecuteTaskResultsMutationVariables = Exact<{
|
||||
taskId: Scalars['ID'];
|
||||
}>;
|
||||
@@ -1508,6 +1522,20 @@ export interface GetSqlExecuteTaskResultsMutation {
|
||||
);
|
||||
}
|
||||
|
||||
export type GetSqlExecutionPlanResultMutationVariables = Exact<{
|
||||
taskId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
export interface GetSqlExecutionPlanResultMutation {
|
||||
result: (
|
||||
Pick<SqlExecutionPlan, 'query'>
|
||||
& { nodes: Array<(
|
||||
Pick<SqlExecutionPlanNode, 'id' | 'parentId' | 'kind' | 'name' | 'type' | 'condition' | 'description'>
|
||||
& { properties: Array<Pick<ObjectPropertyInfo, 'id' | 'category' | 'dataType' | 'description' | 'displayName' | 'features' | 'value' | 'order'>> }
|
||||
)>; }
|
||||
);
|
||||
}
|
||||
|
||||
export type UpdateResultsDataMutationVariables = Exact<{
|
||||
connectionId: Scalars['ID'];
|
||||
contextId: Scalars['ID'];
|
||||
@@ -2383,6 +2411,27 @@ export const AsyncSqlExecuteQueryDocument = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const AsyncSqlExplainExecutionPlanDocument = `
|
||||
mutation asyncSqlExplainExecutionPlan($connectionId: ID!, $contextId: ID!, $query: String!, $configuration: Object!) {
|
||||
taskInfo: asyncSqlExplainExecutionPlan(
|
||||
connectionId: $connectionId
|
||||
contextId: $contextId
|
||||
query: $query
|
||||
configuration: $configuration
|
||||
) {
|
||||
id
|
||||
name
|
||||
running
|
||||
status
|
||||
error {
|
||||
message
|
||||
errorCode
|
||||
stackTrace
|
||||
}
|
||||
taskResult
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const GetSqlExecuteTaskResultsDocument = `
|
||||
mutation getSqlExecuteTaskResults($taskId: ID!) {
|
||||
result: asyncSqlExecuteResults(taskId: $taskId) {
|
||||
@@ -2423,6 +2472,32 @@ export const GetSqlExecuteTaskResultsDocument = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const GetSqlExecutionPlanResultDocument = `
|
||||
mutation getSqlExecutionPlanResult($taskId: ID!) {
|
||||
result: asyncSqlExplainExecutionPlanResult(taskId: $taskId) {
|
||||
query
|
||||
nodes {
|
||||
id
|
||||
parentId
|
||||
kind
|
||||
name
|
||||
type
|
||||
condition
|
||||
description
|
||||
properties {
|
||||
id
|
||||
category
|
||||
dataType
|
||||
description
|
||||
displayName
|
||||
features
|
||||
value
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UpdateResultsDataDocument = `
|
||||
mutation updateResultsData($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $sourceRowValues: [Object]!, $values: Object) {
|
||||
result: updateResultsData(
|
||||
@@ -2833,9 +2908,15 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
|
||||
asyncSqlExecuteQuery(variables: AsyncSqlExecuteQueryMutationVariables): Promise<AsyncSqlExecuteQueryMutation> {
|
||||
return withWrapper(() => client.request<AsyncSqlExecuteQueryMutation>(AsyncSqlExecuteQueryDocument, variables));
|
||||
},
|
||||
asyncSqlExplainExecutionPlan(variables: AsyncSqlExplainExecutionPlanMutationVariables): Promise<AsyncSqlExplainExecutionPlanMutation> {
|
||||
return withWrapper(() => client.request<AsyncSqlExplainExecutionPlanMutation>(AsyncSqlExplainExecutionPlanDocument, variables));
|
||||
},
|
||||
getSqlExecuteTaskResults(variables: GetSqlExecuteTaskResultsMutationVariables): Promise<GetSqlExecuteTaskResultsMutation> {
|
||||
return withWrapper(() => client.request<GetSqlExecuteTaskResultsMutation>(GetSqlExecuteTaskResultsDocument, variables));
|
||||
},
|
||||
getSqlExecutionPlanResult(variables: GetSqlExecutionPlanResultMutationVariables): Promise<GetSqlExecutionPlanResultMutation> {
|
||||
return withWrapper(() => client.request<GetSqlExecutionPlanResultMutation>(GetSqlExecutionPlanResultDocument, variables));
|
||||
},
|
||||
updateResultsData(variables: UpdateResultsDataMutationVariables): Promise<UpdateResultsDataMutation> {
|
||||
return withWrapper(() => client.request<UpdateResultsDataMutation>(UpdateResultsDataDocument, variables));
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface IResultGroup {
|
||||
modelId: string;
|
||||
sqlQueryParams: ISqlQueryParams;
|
||||
order: number;
|
||||
nameOrder: number;
|
||||
}
|
||||
|
||||
export interface ISqlEditorResultTab {
|
||||
@@ -37,8 +38,9 @@ export interface ISqlEditorResultTab {
|
||||
|
||||
export interface IExecutionPlanTab {
|
||||
tabId: string;
|
||||
query: string;
|
||||
executionContext: IDatabaseExecutionContext;
|
||||
query: string;
|
||||
order: number;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -50,5 +52,5 @@ export interface ISqlEditorTabState {
|
||||
tabs: ISqlEditorResultTab[];
|
||||
resultGroups: IResultGroup[];
|
||||
resultTabs: IResultTab[];
|
||||
executionPlanTabs: IExecutionPlanTab[]; // TODO: ex-plan store addition info for tab
|
||||
executionPlanTabs: IExecutionPlanTab[];
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { useTab } from '@cloudbeaver/core-app';
|
||||
import { StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
|
||||
import { Icon, StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
|
||||
import { useController } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
|
||||
import type { SQLCodeEditorController } from './SQLCodeEditor/SQLCodeEditorController';
|
||||
import { SQLCodeEditorLoader } from './SQLCodeEditor/SQLCodeEditorLoader';
|
||||
@@ -44,7 +45,7 @@ const styles = css`
|
||||
background: none;
|
||||
}
|
||||
|
||||
StaticImage {
|
||||
StaticImage, Icon {
|
||||
padding: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@@ -57,6 +58,7 @@ const styles = css`
|
||||
`;
|
||||
|
||||
export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEditorProps) {
|
||||
const translate = useTranslate();
|
||||
const tab = useTab(tabId);
|
||||
const editor = useRef<SQLCodeEditorController>(null);
|
||||
const baseTab = useBaseTab(tabId);
|
||||
@@ -79,8 +81,8 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<sql-editor as="div" className={className}>
|
||||
<actions as="div">
|
||||
<sql-editor className={className}>
|
||||
<actions>
|
||||
<button
|
||||
disabled={controller.isActionsDisabled}
|
||||
onMouseDown={preventFocus}
|
||||
@@ -101,6 +103,17 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd
|
||||
title="Execute SQL in new tab (Ctrl + \\)(Shift + Ctrl + Enter)"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
disabled={!controller.dialect?.supportsExplainExecutionPlan || controller.isActionsDisabled}
|
||||
title={translate('sql_editor_execution_plan_button_tooltip')}
|
||||
onMouseDown={preventFocus}
|
||||
onClick={controller.handleExecutionPlan}
|
||||
>
|
||||
<Icon
|
||||
name="execution-plan"
|
||||
viewBox='0 0 32 32'
|
||||
/>
|
||||
</button>
|
||||
</actions>
|
||||
<SQLCodeEditorLoader
|
||||
ref={editor}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
|
||||
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
|
||||
import { SqlDialectInfoService } from '../SqlDialectInfoService';
|
||||
import { SqlEditorService } from '../SqlEditorService';
|
||||
import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
|
||||
import { SqlQueryService } from '../SqlResultTabs/SqlQueryService';
|
||||
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
|
||||
|
||||
@@ -37,6 +38,9 @@ export class SqlEditorController implements IInitializableController {
|
||||
}
|
||||
|
||||
handleExecute = async (): Promise<void> => {
|
||||
if (this.isActionsDisabled) {
|
||||
return;
|
||||
}
|
||||
this.sqlQueryService.executeEditorQuery(
|
||||
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
|
||||
this.tab.handlerState,
|
||||
@@ -46,6 +50,9 @@ export class SqlEditorController implements IInitializableController {
|
||||
};
|
||||
|
||||
handleExecuteNewTab = async (): Promise<void> => {
|
||||
if (this.isActionsDisabled) {
|
||||
return;
|
||||
}
|
||||
this.sqlQueryService.executeEditorQuery(
|
||||
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
|
||||
this.tab.handlerState,
|
||||
@@ -54,7 +61,16 @@ export class SqlEditorController implements IInitializableController {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: ex-plan add action for execution plan
|
||||
handleExecutionPlan = async (): Promise<void> => {
|
||||
if (this.isActionsDisabled) {
|
||||
return;
|
||||
}
|
||||
this.sqlExecutionPlanService.executeExecutionPlan(
|
||||
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
|
||||
this.tab.handlerState,
|
||||
await this.getExecutingQuery(),
|
||||
);
|
||||
};
|
||||
|
||||
readonly options: EditorConfiguration = {
|
||||
theme: 'material',
|
||||
@@ -71,6 +87,7 @@ export class SqlEditorController implements IInitializableController {
|
||||
// Execute sql script in new tab
|
||||
'Ctrl-\\': () => { this.handleExecuteNewTab(); },
|
||||
'Shift-Ctrl-Enter': () => { this.handleExecuteNewTab(); },
|
||||
'Shift-Ctrl-E': () => { this.handleExecutionPlan(); },
|
||||
|
||||
// Autocomplete
|
||||
'Ctrl-Space': this.showHint.bind(this), // classic for windows, linux
|
||||
@@ -96,7 +113,8 @@ export class SqlEditorController implements IInitializableController {
|
||||
private sqlResultTabsService: SqlResultTabsService,
|
||||
private sqlQueryService: SqlQueryService,
|
||||
private sqlDialectInfoService: SqlDialectInfoService,
|
||||
private sqlEditorService: SqlEditorService
|
||||
private sqlEditorService: SqlEditorService,
|
||||
private sqlExecutionPlanService: SqlExecutionPlanService,
|
||||
) {
|
||||
makeObservable(this, {
|
||||
dialect: computed,
|
||||
|
||||
@@ -246,13 +246,13 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTabClose(tab: ITab<ISqlEditorTabState>) {
|
||||
this.tabExecutionState.delete(tab.id);
|
||||
if (tab.handlerState.executionContext) {
|
||||
await this.sqlEditorService.destroySqlContext(tab.handlerState.executionContext);
|
||||
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
|
||||
import { NestedNode } from './NestedNode';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
TableHeader {
|
||||
composes: theme-background-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
TableHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ExecutionPlanTree: React.FC<Props> = function ExecutionPlanTree({ className }) {
|
||||
const treeContext = useContext(ExecutionPlanTreeContext);
|
||||
|
||||
if (!treeContext) {
|
||||
throw new Error('Tree context must be provided');
|
||||
}
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<Table className={className} selectedItems={treeContext.selectedNodes} onSelect={treeContext.selectNode}>
|
||||
<TableHeader>
|
||||
{treeContext.columns.map(property => (
|
||||
<TableColumnHeader key={property.id || property.order} title={property.displayName}>
|
||||
{property.displayName}
|
||||
</TableColumnHeader>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{treeContext.nodes.map(node => (
|
||||
<NestedNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Split, Pane, ResizerControls, splitStyles } from '@cloudbeaver/core-blocks';
|
||||
import { TextareaNew } from '@cloudbeaver/core-blocks';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ExecutionPlanTree } from './ExecutionPlanTree';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
Pane {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
Split {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
ResizerControls {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
Pane:last-child {
|
||||
flex: 0 0 30%;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ExecutionPlanTreeBlock: React.FC<Props> = function ExecutionPlanTreeBlock({ className, query }) {
|
||||
const style = useStyles(styles, splitStyles);
|
||||
|
||||
return styled(style)(
|
||||
<Split className={className} sticky={30} split='horizontal' keepRatio>
|
||||
<Pane>
|
||||
<ExecutionPlanTree />
|
||||
</Pane>
|
||||
<ResizerControls />
|
||||
<Pane main>
|
||||
<TextareaNew
|
||||
className={className}
|
||||
name='value'
|
||||
rows={3}
|
||||
value={query}
|
||||
readOnly
|
||||
embedded
|
||||
/>
|
||||
</Pane>
|
||||
</Split>
|
||||
);
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
|
||||
|
||||
export interface IExecutionPlanNode extends SqlExecutionPlanNode {
|
||||
children: IExecutionPlanNode[];
|
||||
}
|
||||
|
||||
export interface IExecutionPlanTreeContext {
|
||||
selectedNodes: Map<string, boolean>;
|
||||
readonly columns: ObjectPropertyInfo[];
|
||||
readonly nodes: IExecutionPlanNode[];
|
||||
readonly selectedNode: SqlExecutionPlanNode | undefined;
|
||||
selectNode: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
export const ExecutionPlanTreeContext = createContext<IExecutionPlanTreeContext | null>(null);
|
||||
@@ -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<any, 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<Props> = function Expand({ expanded, onClick, className }) {
|
||||
return styled(useStyles(styles))(
|
||||
<Icon
|
||||
className={className}
|
||||
name='arrow'
|
||||
viewBox='0 0 16 16'
|
||||
onClick={onClick}
|
||||
{...use({ expanded })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { TableItem, TableColumnValue } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ExecutionPlanTreeContext, IExecutionPlanNode } from './ExecutionPlanTreeContext';
|
||||
import { Expand } from './Expand';
|
||||
import { getPropertyValue } from './getPropertyValue';
|
||||
|
||||
const styles = css`
|
||||
TableColumnValue {
|
||||
white-space: pre;
|
||||
cursor: pointer;
|
||||
}
|
||||
expand-container {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 4px;
|
||||
display: flex;
|
||||
}
|
||||
control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
node: IExecutionPlanNode;
|
||||
depth: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const NestedNode: React.FC<Props> = function NestedNode({ node, depth, className }) {
|
||||
const style = useStyles(styles);
|
||||
const treeContext = useContext(ExecutionPlanTreeContext);
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
if (!treeContext) {
|
||||
throw new Error('Tree context must be provided');
|
||||
}
|
||||
|
||||
const expand = useCallback((event: React.MouseEvent<any, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
setExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
return styled(style)(
|
||||
<>
|
||||
<TableItem key={`${node.id}_${depth}`} className={className} item={node.id}>
|
||||
{treeContext.columns.map((column, idx) => {
|
||||
const property = node.properties.find(property => property.id === column.id);
|
||||
const value = property ? getPropertyValue(property) : '';
|
||||
return (
|
||||
<TableColumnValue key={`${property?.id}_${depth}`} title={value}>
|
||||
<control>
|
||||
{idx === 0 && (
|
||||
<>
|
||||
<span>{`${'\t'.repeat(depth)}`}</span>
|
||||
<expand-container>
|
||||
{hasChildren && (
|
||||
<Expand expanded={expanded} onClick={expand} />
|
||||
)}
|
||||
</expand-container>
|
||||
</>
|
||||
)}
|
||||
{value}
|
||||
</control>
|
||||
</TableColumnValue>
|
||||
);
|
||||
})}
|
||||
</TableItem>
|
||||
{expanded && node.children.map(child => (
|
||||
<NestedNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+151
@@ -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<Props> = 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 <TextPlaceholder>{translate('sql_execution_plan_properties_panel_placeholder')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<Table className={className}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader>
|
||||
{translate('sql_execution_plan_properties_panel_name')}
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader>
|
||||
{translate('sql_execution_plan_properties_panel_value')}
|
||||
</TableColumnHeader>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableItem item='general_root' selectDisabled>
|
||||
<TableColumnValue>
|
||||
<control>
|
||||
<expand-container>
|
||||
<Expand expanded={generalExpanded} onClick={() => setGeneralExpanded(!generalExpanded)} />
|
||||
</expand-container>
|
||||
{translate('sql_execution_plan_properties_panel_general')}
|
||||
</control>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue />
|
||||
</TableItem>
|
||||
{generalExpanded && general.map(property => {
|
||||
const name = property.displayName;
|
||||
const value = getPropertyValue(property);
|
||||
return (
|
||||
<TableItem key={property.id} item={property} selectDisabled>
|
||||
<TableColumnValue title={name}>
|
||||
{'\t\t' + name}
|
||||
</TableColumnValue>
|
||||
<TableColumnValue title={value}>
|
||||
{value}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
);
|
||||
})}
|
||||
<TableItem item='details_root' selectDisabled>
|
||||
<TableColumnValue>
|
||||
<control>
|
||||
<expand-container>
|
||||
<Expand expanded={detailsExpanded} onClick={() => setDetailsExpanded(!detailsExpanded)} />
|
||||
</expand-container>
|
||||
{translate('sql_execution_plan_properties_panel_details')}
|
||||
</control>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue />
|
||||
</TableItem>
|
||||
{detailsExpanded && details.map(property => {
|
||||
const name = property.displayName;
|
||||
const value = getPropertyValue(property);
|
||||
return (
|
||||
<TableItem key={property.id} item={property} selectDisabled>
|
||||
<TableColumnValue title={name}>
|
||||
{'\t\t' + name}
|
||||
</TableColumnValue>
|
||||
<TableColumnValue title={value}>
|
||||
{value}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
});
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import type { NotificationService } from '@cloudbeaver/core-events';
|
||||
import {
|
||||
AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecutionPlan
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import {
|
||||
CancellablePromise, cancellableTimeout, Deferred, EDeferredState
|
||||
} from '@cloudbeaver/core-utils';
|
||||
import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
const DELAY_BETWEEN_TRIES = 1000;
|
||||
|
||||
export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
|
||||
private taskId?: string;
|
||||
private timeout?: CancellablePromise<void>;
|
||||
private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
private notificationService: NotificationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async start(
|
||||
query: string,
|
||||
context: IDatabaseExecutionContext,
|
||||
): Promise<void> {
|
||||
// start async task
|
||||
try {
|
||||
const taskInfo = await this.explainExecutionPlanAsync(query, context);
|
||||
await this.applyResult(taskInfo);
|
||||
this.taskId = taskInfo.id;
|
||||
if (this.getState() === EDeferredState.CANCELLING) {
|
||||
await this.cancelAsync(this.taskId);
|
||||
}
|
||||
} catch (e) {
|
||||
this.onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check async task status until execution finished
|
||||
while (this.isInProgress) {
|
||||
if (this.getState() === EDeferredState.CANCELLING) {
|
||||
await this.cancelAsync(this.taskId);
|
||||
}
|
||||
// run the first check immediately because usually the query execution is fast
|
||||
try {
|
||||
const taskInfo = await this.getExecutionPlanStatusAsync(this.taskId);
|
||||
await this.applyResult(taskInfo);
|
||||
if (this.isFinished) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.notificationService.logException(e, 'Failed to check async task status');
|
||||
}
|
||||
|
||||
try {
|
||||
this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
|
||||
await this.timeout;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method just mark process as cancelling
|
||||
* to avoid racing conditions the server request will be executed in sinchronious manner in start method
|
||||
*/
|
||||
async cancel(): Promise<void> {
|
||||
if (this.getState() !== EDeferredState.PENDING) {
|
||||
return;
|
||||
}
|
||||
this.toCancelling();
|
||||
if (this.timeout) {
|
||||
this.timeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private async cancelAsync(taskId: string) {
|
||||
if (this.isCancelConfirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.cancelQueryAsync(taskId);
|
||||
this.isCancelConfirmed = true;
|
||||
} catch (e) {
|
||||
if (this.getState() === EDeferredState.CANCELLING) {
|
||||
this.toPending();
|
||||
this.notificationService.logException(e, 'Failed to cancel async task');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async explainExecutionPlanAsync(
|
||||
query: string,
|
||||
context: IDatabaseExecutionContext,
|
||||
): Promise<AsyncTaskInfo> {
|
||||
const { taskInfo } = await this.graphQLService.sdk.asyncSqlExplainExecutionPlan({
|
||||
connectionId: context.connectionId,
|
||||
contextId: context.contextId,
|
||||
query,
|
||||
configuration: {},
|
||||
});
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private async getExecutionPlanStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
|
||||
const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId, removeOnFinish: false });
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private async cancelQueryAsync(taskId: string): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Split, Pane, ResizerControls, splitStyles, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { EDeferredState } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IExecutionPlanTab } from '../../ISqlEditorTabState';
|
||||
import { ExecutionPlanTreeBlock } from './ExecutionPlanTreeBlock';
|
||||
import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
|
||||
import { PropertiesPanel } from './PropertiesPanel';
|
||||
import { SqlExecutionPlanService } from './SqlExecutionPlanService';
|
||||
import { useExecutionPlanTreeState } from './useExecutionPlanTreeState';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
Pane {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
Split {
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
interface Props {
|
||||
executionPlanTab: IExecutionPlanTab;
|
||||
}
|
||||
|
||||
export const SqlExecutionPlanPanel: React.FC<Props> = observer(function SqlExecutionPlanPanel({
|
||||
executionPlanTab,
|
||||
}) {
|
||||
const style = useStyles(styles, splitStyles);
|
||||
const translate = useTranslate();
|
||||
const tabId = executionPlanTab.tabId;
|
||||
const sqlExecutionPlanService = useService(SqlExecutionPlanService);
|
||||
const executionPlan = sqlExecutionPlanService.results.get(tabId);
|
||||
const process = sqlExecutionPlanService.processes.get(tabId);
|
||||
|
||||
const executionPlanState = useExecutionPlanTreeState(executionPlan?.nodes || []);
|
||||
const loading = process?.isInProgress || !executionPlan;
|
||||
const canCancel = process?.getState() === EDeferredState.PENDING ?? false;
|
||||
|
||||
const cancelTask = useCallback(() => {
|
||||
if (process) {
|
||||
process.cancel();
|
||||
}
|
||||
}, [process]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loader
|
||||
cancelDisabled={!canCancel}
|
||||
onCancel={cancelTask}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!executionPlanState.nodes.length || !executionPlanState.columns.length) {
|
||||
return <TextPlaceholder>{translate('sql_execution_plan_placeholder')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<ExecutionPlanTreeContext.Provider value={executionPlanState}>
|
||||
<Split mode={executionPlanState.selectedNode ? undefined : 'maximize'}>
|
||||
<Pane main>
|
||||
<ExecutionPlanTreeBlock query={executionPlanTab.query} />
|
||||
</Pane>
|
||||
{executionPlanState.selectedNode && (
|
||||
<>
|
||||
<ResizerControls />
|
||||
<Pane>
|
||||
<PropertiesPanel properties={executionPlanState.selectedNode.properties} />
|
||||
</Pane>
|
||||
</>
|
||||
)}
|
||||
</Split>
|
||||
</ExecutionPlanTreeContext.Provider>
|
||||
);
|
||||
});
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { GraphQLService, SqlExecutionPlan } from '@cloudbeaver/core-sdk';
|
||||
import { EDeferredState, uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { ISqlEditorTabState } from '../../ISqlEditorTabState';
|
||||
import { SqlDialectInfoService } from '../../SqlDialectInfoService';
|
||||
import type { SqlExecutionState } from '../../SqlExecutionState';
|
||||
import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess';
|
||||
|
||||
@injectable()
|
||||
export class SqlExecutionPlanService {
|
||||
results: Map<string, SqlExecutionPlan>;
|
||||
processes: Map<string, SQLExecutionPlanProcess>;
|
||||
|
||||
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<void> {
|
||||
if (!editorState.executionContext) {
|
||||
console.error('executeEditorQuery executionContext is not provided');
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = this.createExecutionPlanTab(editorState, query);
|
||||
const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query);
|
||||
const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
|
||||
|
||||
try {
|
||||
this.processes.set(tabId, task);
|
||||
executionState.setExecutionTask(task);
|
||||
editorState.currentTabId = tabId;
|
||||
|
||||
await task.start(
|
||||
subQuery,
|
||||
{
|
||||
connectionId: editorState.executionContext.connectionId,
|
||||
contextId: editorState.executionContext.contextId,
|
||||
}
|
||||
);
|
||||
|
||||
const executionPlan = await task.promise;
|
||||
this.results.set(tabId, executionPlan);
|
||||
} catch (exception) {
|
||||
const message = task.getState() === EDeferredState.CANCELLED ? 'Execution plan process has been canceled' : undefined;
|
||||
this.notificationService.logException(exception, 'Execution plan Error', message);
|
||||
this.removeTab(editorState, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
async getSubQuery(connectionId: string, query: string): Promise<string> {
|
||||
const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId);
|
||||
|
||||
if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) {
|
||||
return query.slice(0, query.length - dialectInfo.scriptDelimiter.length);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private removeTab(state: ISqlEditorTabState, tabId: string) {
|
||||
const tab = state.tabs.find(tab => tab.id === tabId);
|
||||
if (tab) {
|
||||
state.tabs.splice(state.tabs.indexOf(tab), 1);
|
||||
}
|
||||
this.removeExecutionPlanTab(state, tabId);
|
||||
|
||||
if (state.tabs.length > 0) {
|
||||
state.currentTabId = state.tabs[0].id;
|
||||
} else {
|
||||
state.currentTabId = '';
|
||||
}
|
||||
}
|
||||
|
||||
removeExecutionPlanTab(state: ISqlEditorTabState, tabId: string): void {
|
||||
const executionPlanTab = state.executionPlanTabs.find(executionPlanTab => executionPlanTab.tabId === tabId);
|
||||
|
||||
if (executionPlanTab) {
|
||||
state.executionPlanTabs.splice(state.executionPlanTabs.indexOf(executionPlanTab), 1);
|
||||
}
|
||||
|
||||
this.results.delete(tabId);
|
||||
this.processes.delete(tabId);
|
||||
}
|
||||
|
||||
private createExecutionPlanTab(
|
||||
state: ISqlEditorTabState,
|
||||
query: string,
|
||||
) {
|
||||
if (!state.executionContext) {
|
||||
throw new Error('ExecutionContext is not provided');
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
const order = Math.max(0, ...state.tabs.map(tab => tab.order + 1));
|
||||
const nameOrder = Math.max(1, ...state.executionPlanTabs.map(tab => tab.order + 1));
|
||||
|
||||
state.executionPlanTabs.push({
|
||||
tabId: id,
|
||||
executionContext: {
|
||||
connectionId: state.executionContext.connectionId,
|
||||
contextId: state.executionContext.contextId,
|
||||
},
|
||||
query,
|
||||
order: nameOrder,
|
||||
});
|
||||
|
||||
state.tabs.push({
|
||||
id,
|
||||
name: `Execution plan - ${nameOrder}`,
|
||||
icon: 'execution-plan-tab',
|
||||
order,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed, observable } from 'mobx';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IExecutionPlanNode, IExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
|
||||
|
||||
export function isVisibleProperty(property: ObjectPropertyInfo): boolean {
|
||||
return property.features.includes('viewable');
|
||||
}
|
||||
|
||||
interface IPrivateExecutionPlanTreeState extends IExecutionPlanTreeContext {
|
||||
list: SqlExecutionPlanNode[];
|
||||
}
|
||||
|
||||
export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecutionPlanTreeContext {
|
||||
return useObjectRef<IPrivateExecutionPlanTreeState>({
|
||||
list,
|
||||
selectedNodes: new Map(),
|
||||
get columns() {
|
||||
const columns: ObjectPropertyInfo[] = [];
|
||||
|
||||
for (const node of this.list) {
|
||||
for (const property of node.properties) {
|
||||
if (property.id && isVisibleProperty(property) && !columns.find(column => column.id === property.id)) {
|
||||
columns.push(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
},
|
||||
get nodes() {
|
||||
const map: Map<string, number> = new Map();
|
||||
|
||||
const tree: IExecutionPlanNode[] = this.list
|
||||
.map((node, idx) => {
|
||||
map.set(node.id, idx);
|
||||
return ({ ...node, children: [] });
|
||||
});
|
||||
|
||||
const nodes: IExecutionPlanNode[] = [];
|
||||
|
||||
for (const node of tree) {
|
||||
if (node.parentId) {
|
||||
const parent = map.get(node.parentId)!;
|
||||
tree[parent].children.push(node);
|
||||
} else {
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
},
|
||||
get selectedNode(): SqlExecutionPlanNode | undefined {
|
||||
const nodeId = Array.from(this.selectedNodes.keys())[0];
|
||||
return this.list.find(node => node.id === nodeId);
|
||||
},
|
||||
selectNode(nodeId: string) {
|
||||
this.selectedNodes.clear();
|
||||
this.selectedNodes.set(nodeId, true);
|
||||
},
|
||||
}, { list }, {
|
||||
selectedNodes: observable,
|
||||
list: observable,
|
||||
columns: computed,
|
||||
nodes: computed,
|
||||
selectedNode: computed,
|
||||
}, ['selectNode']);
|
||||
}
|
||||
@@ -131,6 +131,7 @@ export class SqlQueryService {
|
||||
tabState: ISqlEditorTabState,
|
||||
modelId: string
|
||||
): IResultGroup {
|
||||
const nameOrder = Math.max(1, ...tabState.resultGroups.map(group => group.nameOrder + 1));
|
||||
const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1));
|
||||
const groupId = uuid();
|
||||
|
||||
@@ -138,6 +139,7 @@ export class SqlQueryService {
|
||||
groupId,
|
||||
modelId,
|
||||
order,
|
||||
nameOrder,
|
||||
sqlQueryParams: params,
|
||||
});
|
||||
|
||||
@@ -208,7 +210,7 @@ export class SqlQueryService {
|
||||
const tab = state.tabs.find(tab => tab.id === resultTab.tabId);
|
||||
|
||||
if (tab) {
|
||||
tab.name = this.getTabNameForOrder(group.order, indexInResultSet, model.source.results.length);
|
||||
tab.name = this.getTabNameForOrder(group.nameOrder, indexInResultSet, model.source.results.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,13 +231,13 @@ export class SqlQueryService {
|
||||
|
||||
state.tabs.push({
|
||||
id,
|
||||
name: this.getTabNameForOrder(group.order, indexInResultSet, results),
|
||||
name: this.getTabNameForOrder(group.nameOrder, indexInResultSet, results),
|
||||
icon: '/icons/grid.png',
|
||||
order: group.order,
|
||||
});
|
||||
}
|
||||
|
||||
private getTabNameForOrder(order: number, indexInResultSet: number, results: number) {
|
||||
return `Result - ${order + 1}` + (results > 1 ? ` (${indexInResultSet + 1})` : '');
|
||||
return `Result - ${order}` + (results > 1 ? ` (${indexInResultSet + 1})` : '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)(
|
||||
<result-panel>
|
||||
{/* TODO: ex-plan add panel */}
|
||||
<SqlExecutionPlanPanel executionPlanTab={executionPlanTab} />
|
||||
</result-panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,12 +28,16 @@ const styles = composes(
|
||||
Tab {
|
||||
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
|
||||
}
|
||||
TabIcon {
|
||||
composes: theme-text-surface from global;
|
||||
}
|
||||
tabs {
|
||||
composes: theme-background-background theme-text-text-primary-on-light from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
wrapper {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
@@ -70,12 +74,12 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
|
||||
})
|
||||
),
|
||||
[tab]
|
||||
);
|
||||
).get();
|
||||
|
||||
const handleOpen = ({ tabId }: ITabData<any>) => navigatorService.openEditorResult(tab.id, tabId);
|
||||
const handleClose = ({ tabId }: ITabData<any>) => navigatorService.closeEditorResult(tab.id, tabId);
|
||||
|
||||
if (!tab.handlerState.resultGroups.length) {
|
||||
if (!tab.handlerState.tabs.length) {
|
||||
return <TextPlaceholder>{translate('sql_editor_placeholder')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
@@ -85,7 +89,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
|
||||
<wrapper>
|
||||
<TabsBox
|
||||
currentTabId={currentId}
|
||||
tabs={orderedTabs.get().map(result => (
|
||||
tabs={orderedTabs.map(result => (
|
||||
<Tab key={result.id} tabId={result.id} onOpen={handleOpen} onClose={handleClose}>
|
||||
<TabIcon icon={result.icon} />
|
||||
<TabTitle>{result.name}</TabTitle>
|
||||
@@ -93,7 +97,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
|
||||
))}
|
||||
style={[styles]}
|
||||
>
|
||||
{orderedTabs.get().map(result => (
|
||||
{orderedTabs.map(result => (
|
||||
<TabPanel key={result.id} tabId={result.id}>
|
||||
<SqlResultPanel tab={tab} id={result.id} />
|
||||
</TabPanel>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export default [
|
||||
['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'],
|
||||
['sql_editor_execution_plan_button_tooltip', 'Explain execution plan (Shift + Ctrl + E)'],
|
||||
['sql_execution_plan_placeholder', 'Execution plan is empty'],
|
||||
['sql_execution_plan_properties_panel_placeholder', 'There are no items to show'],
|
||||
['sql_execution_plan_properties_panel_name', 'Name'],
|
||||
['sql_execution_plan_properties_panel_value', 'Value'],
|
||||
['sql_execution_plan_properties_panel_general', 'General'],
|
||||
['sql_execution_plan_properties_panel_details', 'Details'],
|
||||
];
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export default [
|
||||
['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'],
|
||||
['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'],
|
||||
['sql_execution_plan_placeholder', 'План выполнения пуст'],
|
||||
['sql_execution_plan_properties_panel_placeholder', 'Информация не доступна'],
|
||||
['sql_execution_plan_properties_panel_name', 'Название'],
|
||||
['sql_execution_plan_properties_panel_value', 'Значение'],
|
||||
['sql_execution_plan_properties_panel_general', 'Общие'],
|
||||
['sql_execution_plan_properties_panel_details', 'Дополнительные'],
|
||||
];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SqlEditorBootstrap } from './SqlEditorBootstrap';
|
||||
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
|
||||
import { SqlEditorService } from './SqlEditorService';
|
||||
import { SqlEditorTabService } from './SqlEditorTabService';
|
||||
import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
|
||||
import { SqlQueryService } from './SqlResultTabs/SqlQueryService';
|
||||
import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';
|
||||
|
||||
@@ -27,6 +28,7 @@ export const sqlEditorPluginManifest: PluginManifest = {
|
||||
SqlDialectInfoService,
|
||||
SqlEditorTabService,
|
||||
SqlQueryService,
|
||||
SqlExecutionPlanService,
|
||||
SqlResultTabsService,
|
||||
SqlEditorService,
|
||||
SqlEditorNavigatorService,
|
||||
|
||||
Reference in New Issue
Block a user