feat(plugin-sql-editor): CB-1060 execution plan

This commit is contained in:
Naumov Alexey
2021-06-24 10:30:17 +03:00
parent 8834f1d3fe
commit dff98db98f
26 changed files with 942 additions and 161 deletions
@@ -183,7 +183,21 @@
<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="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: 19 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -25
View File
@@ -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']>;
}
@@ -1431,11 +1431,6 @@ export type ObjectOriginInfoFragment = (
& { details?: Maybe<Array<Pick<ObjectPropertyInfo, 'id' | 'displayName' | 'description' | 'category' | 'dataType' | 'defaultValue' | 'validValues' | 'value' | 'features' | 'order'>>> }
);
export type SqlExecutionPlanNodeFragment = (
Pick<SqlExecutionPlanNode, 'kind' | 'name' | 'type' | 'condition' | 'description'>
& { properties: Array<Pick<ObjectPropertyInfo, 'id' | 'category' | 'dataType' | 'description' | 'displayName' | 'features' | 'value' | 'order'>> }
);
export type SessionStateFragment = Pick<SessionInfo, 'createTime' | 'lastAccessTime' | 'cacheExpired' | 'locale'>;
export type UserConnectionAuthPropertiesFragment = Pick<ObjectPropertyInfo, 'id' | 'displayName' | 'description' | 'category' | 'dataType' | 'value' | 'validValues' | 'defaultValue' | 'features' | 'order'>;
@@ -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
@@ -37,8 +37,8 @@ export interface ISqlEditorResultTab {
export interface IExecutionPlanTab {
tabId: string;
query: string;
executionContext: IDatabaseExecutionContext;
order: number;
options?: Record<string, any>;
}
@@ -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[];
}
@@ -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
/>
</button>
<button
disabled={!controller.dialect?.supportsExplainExecutionPlan}
disabled={!controller.dialect?.supportsExplainExecutionPlan || controller.isActionsDisabled}
onMouseDown={preventFocus}
onClick={controller.handleExecutionPlan}
>
plan
<icon-container title='Open execution plan'>
<Icon
name="execution-plan"
viewBox='0 0 32 32'
/>
</icon-container>
</button>
</actions>
<SQLCodeEditorLoader
@@ -19,7 +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/SqlExecutionPlanService';
import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from '../SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
@@ -38,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,
@@ -47,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,
@@ -56,6 +62,9 @@ export class SqlEditorController implements IInitializableController {
};
handleExecutionPlan = async (): Promise<void> => {
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
@@ -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);
}
}
}
@@ -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<Props> = 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))(
<Table className={className} selectedItems={selectedNodes} onSelect={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>
);
};
@@ -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<Props> = 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)(
<Split className={className} sticky={30} split='horizontal' keepRatio>
<Pane>
<ExecutionPlanTree />
</Pane>
<ResizerControls />
<Pane main>
<QueryPanel />
</Pane>
</Split>
);
};
@@ -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<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,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<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 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<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}>
{treeContext.columns.map((column, idx) => {
const property = node.properties.find(property => property.id === column.id);
const value = getValue(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}
/>
))}
</>
);
};
@@ -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>
);
});
@@ -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<Props> = function QueryPanel({ className }) {
const treeContext = useContext(ExecutionPlanTreeContext);
if (!treeContext) {
throw new Error('Tree context must be provided');
}
return (
<TextareaNew
className={className}
name='value'
rows={3}
value={treeContext.query}
readOnly
embedded
/>
);
};
@@ -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<SqlExecutionPlan> {
): Promise<void> {
// 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<SqlExecutionPlan> {
}
// 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<SqlExecutionPlan> {
}
}
private async executeQueryAsync(
private async explainExecutionPlanAsync(
query: string,
context: IDatabaseExecutionContext,
): Promise<AsyncTaskInfo> {
@@ -116,7 +116,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
return taskInfo;
}
private async getQueryStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
private async getExecutionPlanStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId, removeOnFinish: false });
return taskInfo;
}
@@ -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<Props> = 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 (
<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.metadataPanel ? undefined : 'maximize'}>
<Pane main>
<ExecutionPlanTreeBlock />
</Pane>
{executionPlanState.metadataPanel && executionPlanState.selectedNode && (
<>
<ResizerControls />
<Pane>
<PropertiesPanel properties={executionPlanState.selectedNode.properties} />
</Pane>
</>
)}
</Split>
</ExecutionPlanTreeContext.Provider>
);
});
@@ -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<string, IExecutionPlanData>;
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);
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<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) {
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;
}
}
@@ -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;
}
@@ -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<IState>({
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<string, number> = 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,
});
}
@@ -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<void> {
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<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;
}
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,
});
}
}
@@ -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({
@@ -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>
<div>{executionPlanTab.query}</div>
<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%;
@@ -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<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 +104,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
<wrapper>
<TabsBox
currentTabId={currentId}
tabs={orderedTabs.get().map(result => (
tabs={categorizedTabs.map(result => (
<Tab key={result.id} tabId={result.id} onOpen={handleOpen} onClose={handleClose}>
<TabIcon icon={result.icon} />
<TabTitle>{result.name}</TabTitle>
@@ -93,7 +112,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
))}
style={[styles]}
>
{orderedTabs.get().map(result => (
{categorizedTabs.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,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'],
];
@@ -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', 'Детали'],
];
@@ -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';