mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-29 18:35:19 +08:00
refactor(plugin-sql-editor): CB-1073 remove execution plan tree context
This commit is contained in:
@@ -192,15 +192,12 @@
|
||||
<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 id="execution-plan" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 4C17.0886 4 17.173 4.01772 17.25 4.0498L19 4.04981V5.04981H20V8.04981H18.1501L18.1501 18.3956C18.1501 19.3068 17.4114 20.0456 16.5002 20.0456L14 20.0456V20.0498H5V17.0498H14V18.7456L16.5002 18.7456C16.6934 18.7456 16.8501 18.5889 16.8501 18.3956L16.8501 6.04981L9.15 6.0498V14.0497V14.6997H7.85V14.0497V5.65C7.85 4.73873 8.58873 4 9.5 4H17Z" fill="#338ECC"/>
|
||||
<rect x="14" y="8.0498" width="2" height="2" fill="#338ECC"/>
|
||||
<rect x="14" y="12.0498" width="2" height="2" fill="#338ECC"/>
|
||||
<rect x="10" y="8.0498" width="2" height="2" fill="#338ECC"/>
|
||||
<rect x="10" y="12.0498" width="2" height="2" fill="#338ECC"/>
|
||||
</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"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@@ -39,7 +39,6 @@ export interface ISqlEditorResultTab {
|
||||
export interface IExecutionPlanTab {
|
||||
tabId: string;
|
||||
executionContext: IDatabaseExecutionContext;
|
||||
query: string;
|
||||
order: number;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -85,35 +85,30 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd
|
||||
<actions>
|
||||
<button
|
||||
disabled={controller.isActionsDisabled}
|
||||
title={translate('sql_editor_sql_execution_button_tooltip')}
|
||||
onMouseDown={preventFocus}
|
||||
onClick={controller.handleExecute}
|
||||
>
|
||||
<StaticImage
|
||||
icon="/icons/sql_exec.png"
|
||||
title="Execute SQL Statement (Ctrl + Enter)"
|
||||
/>
|
||||
<StaticImage icon="/icons/sql_exec.png" />
|
||||
</button>
|
||||
<button
|
||||
disabled={controller.isActionsDisabled}
|
||||
title={translate('sql_editor_sql_execution_new_tab_button_tooltip')}
|
||||
onMouseDown={preventFocus}
|
||||
onClick={controller.handleExecuteNewTab}
|
||||
>
|
||||
<StaticImage
|
||||
icon="/icons/sql_exec_new.png"
|
||||
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'
|
||||
/>
|
||||
<StaticImage icon="/icons/sql_exec_new.png" />
|
||||
</button>
|
||||
{controller.dialect?.supportsExplainExecutionPlan && (
|
||||
<button
|
||||
disabled={controller.isActionsDisabled}
|
||||
title={translate('sql_editor_execution_plan_button_tooltip')}
|
||||
onMouseDown={preventFocus}
|
||||
onClick={controller.handleExecutionPlan}
|
||||
>
|
||||
<Icon name="execution-plan" viewBox='0 0 24 24' />
|
||||
</button>
|
||||
)}
|
||||
</actions>
|
||||
<SQLCodeEditorLoader
|
||||
ref={editor}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class SqlEditorController implements IInitializableController {
|
||||
};
|
||||
|
||||
handleExecutionPlan = async (): Promise<void> => {
|
||||
if (this.isActionsDisabled) {
|
||||
if (this.isActionsDisabled || !this.dialect?.supportsExplainExecutionPlan) {
|
||||
return;
|
||||
}
|
||||
this.sqlExecutionPlanService.executeExecutionPlan(
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import 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>
|
||||
);
|
||||
};
|
||||
+50
-6
@@ -6,21 +6,35 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Split, Pane, ResizerControls, splitStyles } from '@cloudbeaver/core-blocks';
|
||||
import { TextareaNew } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Split, Pane, ResizerControls, splitStyles, TextPlaceholder,
|
||||
Table, TableHeader, TableColumnHeader, TableBody, TextareaNew
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ExecutionPlanTree } from './ExecutionPlanTree';
|
||||
import { NestedNode } from './NestedNode';
|
||||
import { useExecutionPlanTreeState } from './useExecutionPlanTreeState';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
Pane {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
TableHeader {
|
||||
composes: theme-background-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
TableHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
Split {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
@@ -39,17 +53,47 @@ const styles = composes(
|
||||
);
|
||||
|
||||
interface Props {
|
||||
nodeList: SqlExecutionPlanNode[];
|
||||
query: string;
|
||||
onNodeSelect: (nodeId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ExecutionPlanTreeBlock: React.FC<Props> = function ExecutionPlanTreeBlock({ className, query }) {
|
||||
export const ExecutionPlanTreeBlock: React.FC<Props> = observer(function ExecutionPlanTreeBlock({
|
||||
nodeList, query, onNodeSelect, className,
|
||||
}) {
|
||||
const style = useStyles(styles, splitStyles);
|
||||
const translate = useTranslate();
|
||||
const { columns, nodes, selectedNodes, selectNode } = useExecutionPlanTreeState(nodeList, onNodeSelect);
|
||||
|
||||
return styled(style)(
|
||||
<Split className={className} sticky={30} split='horizontal' keepRatio>
|
||||
<Pane>
|
||||
<ExecutionPlanTree />
|
||||
{nodes.length && columns.length ? (
|
||||
<Table selectedItems={selectedNodes} onSelect={selectNode}>
|
||||
<TableHeader>
|
||||
{columns.map(property => {
|
||||
const name = property.displayName;
|
||||
const columnTooltip = `${name} ${property.description ? '(' + property.description + ')' : ''}`;
|
||||
return (
|
||||
<TableColumnHeader key={property.id || property.order} title={columnTooltip}>
|
||||
{name}
|
||||
</TableColumnHeader>
|
||||
);
|
||||
})}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{nodes.map(node => (
|
||||
<NestedNode
|
||||
key={node.id}
|
||||
columns={columns}
|
||||
node={node}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : <TextPlaceholder>{translate('sql_execution_plan_placeholder')}</TextPlaceholder>}
|
||||
</Pane>
|
||||
<ResizerControls />
|
||||
<Pane main>
|
||||
@@ -64,4 +108,4 @@ export const ExecutionPlanTreeBlock: React.FC<Props> = function ExecutionPlanTre
|
||||
</Pane>
|
||||
</Split>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ export interface IExecutionPlanTreeContext {
|
||||
selectedNodes: Map<string, boolean>;
|
||||
readonly columns: ObjectPropertyInfo[];
|
||||
readonly nodes: IExecutionPlanNode[];
|
||||
readonly selectedNode: SqlExecutionPlanNode | undefined;
|
||||
selectNode: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { useCallback, 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 type { IExecutionPlanNode } from './ExecutionPlanTreeContext';
|
||||
import { Expand } from './Expand';
|
||||
import { getPropertyValue } from './getPropertyValue';
|
||||
|
||||
@@ -34,20 +35,16 @@ const styles = css`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
columns: ObjectPropertyInfo[];
|
||||
node: IExecutionPlanNode;
|
||||
depth: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const NestedNode: React.FC<Props> = function NestedNode({ node, depth, className }) {
|
||||
export const NestedNode: React.FC<Props> = function NestedNode({ columns, 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);
|
||||
@@ -58,11 +55,11 @@ export const NestedNode: React.FC<Props> = function NestedNode({ node, depth, cl
|
||||
return styled(style)(
|
||||
<>
|
||||
<TableItem key={`${node.id}_${depth}`} className={className} item={node.id}>
|
||||
{treeContext.columns.map((column, idx) => {
|
||||
{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}>
|
||||
<TableColumnValue key={`${property?.id}_${depth}`} title={value || undefined}>
|
||||
<control>
|
||||
{idx === 0 && (
|
||||
<>
|
||||
@@ -83,6 +80,7 @@ export const NestedNode: React.FC<Props> = function NestedNode({ node, depth, cl
|
||||
{expanded && node.children.map(child => (
|
||||
<NestedNode
|
||||
key={child.id}
|
||||
columns={columns}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
|
||||
-151
@@ -1,151 +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 { 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>
|
||||
);
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Table, TableHeader, TableColumnHeader, TableBody, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { isVisibleProperty } from '../useExecutionPlanTreeState';
|
||||
import { PropertiesPanelItem } from './PropertiesPanelItem';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
TableColumnHeader {
|
||||
composes: theme-background-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
TableColumnHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
interface Props {
|
||||
selectedNode: string;
|
||||
nodeList: SqlExecutionPlanNode[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PropertiesPanel: React.FC<Props> = observer(function PropertiesPanel({ selectedNode, nodeList, className }) {
|
||||
const style = useStyles(styles);
|
||||
const translate = useTranslate();
|
||||
|
||||
const { general, details } = useMemo(() => computed(() => {
|
||||
const general: ObjectPropertyInfo[] = [];
|
||||
const details: ObjectPropertyInfo[] = [];
|
||||
|
||||
const node = nodeList.find(node => node.id === selectedNode);
|
||||
|
||||
if (!node) {
|
||||
return { general, details };
|
||||
}
|
||||
|
||||
for (const property of node.properties) {
|
||||
if (isVisibleProperty(property)) {
|
||||
general.push(property);
|
||||
} else {
|
||||
details.push(property);
|
||||
}
|
||||
}
|
||||
|
||||
return { general, details };
|
||||
}), [selectedNode, nodeList]).get();
|
||||
|
||||
if (!general.length && !details.length) {
|
||||
return <TextPlaceholder>{translate('sql_execution_plan_properties_panel_placeholder')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
const nameColumnTitle = translate('sql_execution_plan_properties_panel_name');
|
||||
const valueColumnTitle = translate('sql_execution_plan_properties_panel_value');
|
||||
|
||||
return styled(style)(
|
||||
<Table className={className}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader title={nameColumnTitle}>
|
||||
{nameColumnTitle}
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader title={valueColumnTitle}>
|
||||
{valueColumnTitle}
|
||||
</TableColumnHeader>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<PropertiesPanelItem properties={general} rootTitle={translate('sql_execution_plan_properties_panel_general')} />
|
||||
<PropertiesPanelItem properties={details} rootTitle={translate('sql_execution_plan_properties_panel_details')} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
});
|
||||
+73
@@ -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 { useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { TableItem, TableColumnValue } from '@cloudbeaver/core-blocks';
|
||||
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { Expand } from '../Expand';
|
||||
import { getPropertyValue } from '../getPropertyValue';
|
||||
|
||||
const styles = css`
|
||||
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[];
|
||||
rootTitle: string;
|
||||
}
|
||||
|
||||
export const PropertiesPanelItem: React.FC<Props> = function PropertiesPanelItem({ properties, rootTitle }) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
return styled(styles)(
|
||||
<>
|
||||
<TableItem item={`${rootTitle}_root`} selectDisabled>
|
||||
<TableColumnValue title={rootTitle}>
|
||||
<control>
|
||||
<expand-container>
|
||||
<Expand expanded={expanded} onClick={() => setExpanded(!expanded)} />
|
||||
</expand-container>
|
||||
{rootTitle}
|
||||
</control>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue />
|
||||
</TableItem>
|
||||
{expanded && properties.map(property => {
|
||||
const name = property.displayName;
|
||||
const tooltip = `${name} ${property.description ? '(' + property.description + ')' : ''}`;
|
||||
const value = getPropertyValue(property);
|
||||
return (
|
||||
<TableItem key={property.id} item={property.id} selectDisabled>
|
||||
<TableColumnValue title={tooltip}>
|
||||
{'\t\t' + name}
|
||||
</TableColumnValue>
|
||||
<TableColumnValue title={value}>
|
||||
{value}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+25
-42
@@ -7,22 +7,19 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Split, Pane, ResizerControls, splitStyles, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { Split, Pane, ResizerControls, splitStyles } 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 { PropertiesPanel } from './PropertiesPanel/PropertiesPanel';
|
||||
import { SqlExecutionPlanService } from './SqlExecutionPlanService';
|
||||
import { useExecutionPlanTreeState } from './useExecutionPlanTreeState';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
@@ -48,50 +45,36 @@ export const SqlExecutionPlanPanel: React.FC<Props> = observer(function SqlExecu
|
||||
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 data = sqlExecutionPlanService.data.get(executionPlanTab.tabId);
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null);
|
||||
|
||||
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) {
|
||||
if (data?.process.isInProgress || !data?.executionPlan) {
|
||||
return (
|
||||
<Loader
|
||||
cancelDisabled={!canCancel}
|
||||
onCancel={cancelTask}
|
||||
cancelDisabled={data?.process.getState() !== EDeferredState.PENDING}
|
||||
onCancel={() => data?.process.cancel()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 : 'minimize'} sticky={30}>
|
||||
<Pane>
|
||||
<ExecutionPlanTreeBlock query={executionPlanTab.query} />
|
||||
</Pane>
|
||||
{executionPlanState.selectedNode && (
|
||||
<>
|
||||
<ResizerControls />
|
||||
<Pane main>
|
||||
<PropertiesPanel properties={executionPlanState.selectedNode.properties} />
|
||||
</Pane>
|
||||
</>
|
||||
)}
|
||||
</Split>
|
||||
</ExecutionPlanTreeContext.Provider>
|
||||
<Split mode={selectedNode ? undefined : 'minimize'} sticky={30}>
|
||||
<Pane>
|
||||
<ExecutionPlanTreeBlock
|
||||
nodeList={data.executionPlan.nodes}
|
||||
query={data.executionPlan.query}
|
||||
onNodeSelect={setSelectedNode}
|
||||
/>
|
||||
</Pane>
|
||||
{selectedNode && (
|
||||
<>
|
||||
<ResizerControls />
|
||||
<Pane main>
|
||||
<PropertiesPanel selectedNode={selectedNode} nodeList={data.executionPlan.nodes} />
|
||||
</Pane>
|
||||
</>
|
||||
)}
|
||||
</Split>
|
||||
);
|
||||
});
|
||||
|
||||
+19
-16
@@ -18,10 +18,14 @@ import { SqlDialectInfoService } from '../../SqlDialectInfoService';
|
||||
import type { SqlExecutionState } from '../../SqlExecutionState';
|
||||
import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess';
|
||||
|
||||
interface IExecutionPlanData {
|
||||
process: SQLExecutionPlanProcess;
|
||||
executionPlan: SqlExecutionPlan | null;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class SqlExecutionPlanService {
|
||||
results: Map<string, SqlExecutionPlan>;
|
||||
processes: Map<string, SQLExecutionPlanProcess>;
|
||||
data: Map<string, IExecutionPlanData>;
|
||||
|
||||
constructor(
|
||||
private sqlDialectInfoService: SqlDialectInfoService,
|
||||
@@ -29,11 +33,9 @@ export class SqlExecutionPlanService {
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
makeObservable(this, {
|
||||
results: observable,
|
||||
processes: observable,
|
||||
data: observable,
|
||||
});
|
||||
this.results = new Map();
|
||||
this.processes = new Map();
|
||||
this.data = new Map();
|
||||
}
|
||||
|
||||
async executeExecutionPlan(
|
||||
@@ -46,12 +48,15 @@ export class SqlExecutionPlanService {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = this.createExecutionPlanTab(editorState, query);
|
||||
const tabId = this.createExecutionPlanTab(editorState);
|
||||
const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query);
|
||||
const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
|
||||
this.data.set(tabId, {
|
||||
process: task,
|
||||
executionPlan: null,
|
||||
});
|
||||
|
||||
try {
|
||||
this.processes.set(tabId, task);
|
||||
executionState.setExecutionTask(task);
|
||||
editorState.currentTabId = tabId;
|
||||
|
||||
@@ -64,7 +69,10 @@ export class SqlExecutionPlanService {
|
||||
);
|
||||
|
||||
const executionPlan = await task.promise;
|
||||
this.results.set(tabId, executionPlan);
|
||||
this.data.set(tabId, {
|
||||
process: task,
|
||||
executionPlan,
|
||||
});
|
||||
} catch (exception) {
|
||||
const message = task.getState() === EDeferredState.CANCELLED ? 'Execution plan process has been canceled' : undefined;
|
||||
this.notificationService.logException(exception, 'Execution plan Error', message);
|
||||
@@ -103,14 +111,10 @@ export class SqlExecutionPlanService {
|
||||
state.executionPlanTabs.splice(state.executionPlanTabs.indexOf(executionPlanTab), 1);
|
||||
}
|
||||
|
||||
this.results.delete(tabId);
|
||||
this.processes.delete(tabId);
|
||||
this.data.delete(tabId);
|
||||
}
|
||||
|
||||
private createExecutionPlanTab(
|
||||
state: ISqlEditorTabState,
|
||||
query: string,
|
||||
) {
|
||||
private createExecutionPlanTab(state: ISqlEditorTabState) {
|
||||
if (!state.executionContext) {
|
||||
throw new Error('ExecutionContext is not provided');
|
||||
}
|
||||
@@ -125,7 +129,6 @@ export class SqlExecutionPlanService {
|
||||
connectionId: state.executionContext.connectionId,
|
||||
contextId: state.executionContext.contextId,
|
||||
},
|
||||
query,
|
||||
order: nameOrder,
|
||||
});
|
||||
|
||||
|
||||
+12
-12
@@ -18,17 +18,21 @@ export function isVisibleProperty(property: ObjectPropertyInfo): boolean {
|
||||
}
|
||||
|
||||
interface IPrivateExecutionPlanTreeState extends IExecutionPlanTreeContext {
|
||||
list: SqlExecutionPlanNode[];
|
||||
nodeList: SqlExecutionPlanNode[];
|
||||
onNodeSelect: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecutionPlanTreeContext {
|
||||
export function useExecutionPlanTreeState(
|
||||
nodeList: SqlExecutionPlanNode[], onNodeSelect: (nodeId: string) => void
|
||||
): IExecutionPlanTreeContext {
|
||||
return useObjectRef<IPrivateExecutionPlanTreeState>({
|
||||
list,
|
||||
nodeList,
|
||||
onNodeSelect,
|
||||
selectedNodes: new Map(),
|
||||
get columns() {
|
||||
const columns: ObjectPropertyInfo[] = [];
|
||||
|
||||
for (const node of this.list) {
|
||||
for (const node of this.nodeList) {
|
||||
for (const property of node.properties) {
|
||||
if (property.id && isVisibleProperty(property) && !columns.find(column => column.id === property.id)) {
|
||||
columns.push(property);
|
||||
@@ -41,7 +45,7 @@ export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecut
|
||||
get nodes() {
|
||||
const map: Map<string, number> = new Map();
|
||||
|
||||
const tree: IExecutionPlanNode[] = this.list
|
||||
const tree: IExecutionPlanNode[] = this.nodeList
|
||||
.map((node, idx) => {
|
||||
map.set(node.id, idx);
|
||||
return ({ ...node, children: [] });
|
||||
@@ -60,19 +64,15 @@ export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[]): IExecut
|
||||
|
||||
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);
|
||||
this.onNodeSelect(nodeId);
|
||||
},
|
||||
}, { list }, {
|
||||
}, { nodeList, onNodeSelect }, {
|
||||
selectedNodes: observable,
|
||||
list: observable,
|
||||
nodeList: observable,
|
||||
columns: computed,
|
||||
nodes: computed,
|
||||
selectedNode: computed,
|
||||
}, ['selectNode']);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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_editor_sql_execution_button_tooltip', 'Execute SQL Statement (Ctrl + Enter)'],
|
||||
['sql_editor_sql_execution_new_tab_button_tooltip', 'Execute SQL Statement in new tab (Ctrl + \\)(Shift + Ctrl + Enter)'],
|
||||
['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'],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export default [
|
||||
['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'],
|
||||
['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'],
|
||||
['sql_editor_sql_execution_button_tooltip', 'Выполнить SQL Выражение (Ctrl + Enter)'],
|
||||
['sql_editor_sql_execution_new_tab_button_tooltip', 'Выполнить SQL Выражение в новой вкладке (Ctrl + \\)(Shift + Ctrl + Enter)'],
|
||||
['sql_execution_plan_placeholder', 'План выполнения пуст'],
|
||||
['sql_execution_plan_properties_panel_placeholder', 'Информация не доступна'],
|
||||
['sql_execution_plan_properties_panel_name', 'Название'],
|
||||
|
||||
Reference in New Issue
Block a user