mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-24 11:35:54 +08:00
feat(plugin-sql-editor): CB-1060 execution plan
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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 { 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>
|
||||
);
|
||||
};
|
||||
+63
@@ -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>
|
||||
);
|
||||
};
|
||||
+26
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+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>
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
+5
-5
@@ -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;
|
||||
}
|
||||
+93
@@ -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>
|
||||
);
|
||||
});
|
||||
+146
@@ -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;
|
||||
}
|
||||
+85
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user