CB-3300 feat: dataset editing (#1614)

* CB-3300 feat: dataset editing

* CB-3300 chore: remove unused styles

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
Alexey
2023-04-19 19:01:44 +03:00
committed by GitHub
co-authored by mr-anton-t
parent 200105f692
commit 736fd39d08
34 changed files with 304 additions and 64 deletions
@@ -22,6 +22,7 @@ import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BI
import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT';
import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN';
import { ScriptPreviewService } from './ScriptPreview/ScriptPreviewService';
import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures';
import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA';
@injectable()
@@ -53,6 +54,29 @@ export class MenuBootstrap extends Bootstrap {
return false;
}
if (
!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.executable)
&& [
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
ACTION_SQL_EDITOR_EXECUTE_SCRIPT,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
].includes(action)
) {
return false;
}
if (
!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query)
&& [
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
].includes(action)
) {
return false;
}
return (
[
ACTION_SQL_EDITOR_EXECUTE,
@@ -86,7 +110,13 @@ export class MenuBootstrap extends Bootstrap {
this.keyBindingService.addKeyBindingHandler({
id: 'sql-editor-execute-script',
binding: KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT,
isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT,
isBindingApplicable: (contexts, action) => {
const sqlEditorData = contexts.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA);
return (
action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT
&& sqlEditorData?.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) === true
);
},
handler: this.sqlEditorActionHandler.bind(this),
});
@@ -40,9 +40,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
}
get features(): ESqlDataSourceFeatures[] {
return [ESqlDataSourceFeatures.script];
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable];
}
protected saved: boolean;
readonly history: ISqlDataSourceHistory;
readonly onUpdate: ISyncExecutor;
readonly onSetScript: ISyncExecutor<ISetScriptData>;
@@ -57,6 +58,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.message = undefined;
this.outdated = true;
this.editing = true;
this.saved = true;
this.history = new SqlDataSourceHistory();
this.onUpdate = new SyncExecutor();
this.onSetScript = new SyncExecutor();
@@ -83,17 +85,24 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
}
setScript(script: string, source?: string): void {
this.saved = false;
this.onSetScript.execute({ script, source });
}
abstract canRename(name: string | null): boolean;
setName(name: string | null): void {
this.onUpdate.execute();
}
setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void {
this.onUpdate.execute();
}
isSaved(): boolean {
return this.saved;
}
isError(): boolean {
return isContainsException(this.exception);
}
@@ -146,6 +155,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.onUpdate.execute();
}
save(): Promise<void> | void { }
load(): Promise<void> | void { }
reset(): Promise<void> | void {
this.saved = true;
}
dispose(): void | Promise<void> { }
}
@@ -8,6 +8,8 @@
export enum ESqlDataSourceFeatures {
script,
query,
executable,
setName,
setProject
}
@@ -40,6 +40,7 @@ export interface ISqlDataSource extends ILoadableState {
readonly features: ESqlDataSourceFeatures[];
readonly history: ISqlDataSourceHistory;
isSaved(): boolean;
isReadonly(): boolean;
isEditing(): boolean;
isOutdated(): boolean;
@@ -52,6 +53,8 @@ export interface ISqlDataSource extends ILoadableState {
setScript(script: string, source?: string): void;
setEditing(state: boolean): void;
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
save(): Promise<void> | void;
load(): Promise<void> | void;
reset(): Promise<void> | void;
dispose(): Promise<void> | void;
}
@@ -30,7 +30,12 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
}
get features(): ESqlDataSourceFeatures[] {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName];
return [
ESqlDataSourceFeatures.script,
ESqlDataSourceFeatures.query,
ESqlDataSourceFeatures.executable,
ESqlDataSourceFeatures.setName,
];
}
private readonly state: ILocalStorageSqlDataSourceState;
@@ -47,6 +52,10 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
});
}
isSaved(): boolean {
return true;
}
isReadonly(): boolean {
return false;
}
@@ -29,7 +29,12 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
}
get features(): ESqlDataSourceFeatures[] {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName];
return [
ESqlDataSourceFeatures.script,
ESqlDataSourceFeatures.query,
ESqlDataSourceFeatures.executable,
ESqlDataSourceFeatures.setName,
];
}
private _name: string | null;
@@ -55,6 +60,10 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
});
}
isSaved(): boolean {
return true;
}
isReadonly(): boolean {
return false;
}
@@ -44,9 +44,7 @@ export const SQL_EDITOR_ACTIONS_MENU_STYLES = css`
}
& menu-bar-item-label {
display: block;
text-transform: uppercase;
font-weight: 700;
display: none;
}
& IconOrImage + menu-bar-item-label, & Loader + menu-bar-item-label {
@@ -0,0 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { createMenu } from '@cloudbeaver/core-view';
export const SQL_EDITOR_TOOLS_MENU = createMenu('sql-editor-tools', '');
@@ -20,6 +20,7 @@ import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'
import { ISqlEditorModeProps, SqlEditorModeService } from '../SqlEditorModeService';
import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA';
import type { ISqlEditorProps } from './ISqlEditorProps';
import { SqlEditorActionsMenu } from './SqlEditorActionsMenu';
import { SqlEditorTools } from './SqlEditorTools';
import { useSqlEditor } from './useSqlEditor';
@@ -53,7 +54,7 @@ const styles = css`
user-select: none;
&:empty {
display: none;
width: initial;
}
}
@@ -124,10 +125,16 @@ export const SqlEditor = observer<ISqlEditorProps>(function SqlEditor({ state, c
const displayedEditors = getComputed(() => sqlEditorModeService.tabsContainer.getDisplayed({ state, data }).length);
useEffect(() => {
split.fixate('maximize', displayedEditors === 0);
if (displayedEditors === 0) {
split.fixate('maximize', true);
} else if (split.state.disable) {
split.fixate('resize', false);
split.state.setSize(-1);
}
});
const isScript = data.dataSource?.hasFeature(ESqlDataSourceFeatures.script);
const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
return styled(styles, BASE_TAB_STYLES, VERTICAL_ROTATED_TAB_STYLES, tabStyles)(
<TabsState
@@ -142,22 +149,26 @@ export const SqlEditor = observer<ISqlEditorProps>(function SqlEditor({ state, c
<sql-editor className={className}>
<container>
<actions onMouseDown={preventFocusHandler}>
{isScript && (
{isExecutable && (
<>
<button
disabled={disabled}
title={translate('sql_editor_sql_execution_button_tooltip')}
onClick={data.executeQuery}
>
<StaticImage icon="/icons/sql_exec.svg" />
</button>
<button
disabled={disabled}
title={translate('sql_editor_sql_execution_new_tab_button_tooltip')}
onClick={data.executeQueryNewTab}
>
<StaticImage icon="/icons/sql_exec_new.svg" />
</button>
{isQuery && (
<>
<button
disabled={disabled}
title={translate('sql_editor_sql_execution_button_tooltip')}
onClick={data.executeQuery}
>
<StaticImage icon="/icons/sql_exec.svg" />
</button>
<button
disabled={disabled}
title={translate('sql_editor_sql_execution_new_tab_button_tooltip')}
onClick={data.executeQueryNewTab}
>
<StaticImage icon="/icons/sql_exec_new.svg" />
</button>
</>
)}
<button
disabled={disabled}
title={translate('sql_editor_sql_execution_script_button_tooltip')}
@@ -166,7 +177,7 @@ export const SqlEditor = observer<ISqlEditorProps>(function SqlEditor({ state, c
>
<StaticImage icon="/icons/sql_script_exec.svg" />
</button>
{data.dialect?.supportsExplainExecutionPlan && (
{isQuery && data.dialect?.supportsExplainExecutionPlan && (
<button
disabled={disabled}
title={translate('sql_editor_execution_plan_button_tooltip')}
@@ -177,6 +188,7 @@ export const SqlEditor = observer<ISqlEditorProps>(function SqlEditor({ state, c
)}
</>
)}
<SqlEditorActionsMenu state={state} />
</actions>
<SqlEditorTools data={data} state={state} style={styles} />
</container>
@@ -28,6 +28,6 @@ export const SqlEditorActionsMenu = observer<Props>(function SqlEditorActionsMen
menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state);
return (
<MenuBar style={[SQL_EDITOR_ACTIONS_MENU_STYLES, style]} menu={menu} />
<MenuBar style={[SQL_EDITOR_ACTIONS_MENU_STYLES, ...[style].flat(2)]} menu={menu} />
);
});
@@ -15,7 +15,7 @@ import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures';
import type { ISQLEditorData } from './ISQLEditorData';
import { SqlEditorActionsMenu } from './SqlEditorActionsMenu';
import { SqlEditorToolsMenu } from './SqlEditorToolsMenu';
import { useTools } from './useTools';
const styles = css`
@@ -73,19 +73,17 @@ export const SqlEditorTools = observer<Props>(function SqlEditorTools({
return styled(useStyles(style, styles))(
<tools className={className} onMouseDown={preventFocusHandler}>
<SqlEditorActionsMenu state={state} />
<SqlEditorToolsMenu state={state} />
{isScript && (
<>
{!data.readonly && (
<button
disabled={disabled}
title={translate('sql_editor_sql_format_button_tooltip')}
hidden={isActiveSegmentMode}
onClick={data.formatScript}
>
<StaticImage icon="/icons/sql_format_sm.svg" />
</button>
)}
<button
disabled={disabled || data.readonly}
title={translate('sql_editor_sql_format_button_tooltip')}
hidden={isActiveSegmentMode}
onClick={data.formatScript}
>
<StaticImage icon="/icons/sql_format_sm.svg" />
</button>
<button
disabled={scriptEmpty}
title={translate('sql_editor_download_script_tooltip')}
@@ -94,14 +92,15 @@ export const SqlEditorTools = observer<Props>(function SqlEditorTools({
>
<StaticImage icon='/icons/export.svg' />
</button>
{!isActiveSegmentMode && !data.readonly && (
{!isActiveSegmentMode && (
<UploadArea
accept='.sql'
title={translate('sql_editor_upload_script_tooltip')}
disabled={data.readonly}
reset
onChange={handleScriptUpload}
>
<upload>
<upload disabled={data.readonly}>
<StaticImage icon='/icons/import.svg' />
</upload>
</UploadArea>
@@ -0,0 +1,33 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 type { ComponentStyle } from '@cloudbeaver/core-theming';
import { MenuBar } from '@cloudbeaver/core-ui';
import { IDataContext, useMenu } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SQL_EDITOR_ACTIONS_MENU_STYLES } from './SQL_EDITOR_ACTIONS_MENU_STYLES';
import { SQL_EDITOR_TOOLS_MENU } from './SQL_EDITOR_TOOLS_MENU';
interface Props {
state: ISqlEditorTabState;
context?: IDataContext;
style?: ComponentStyle;
}
export const SqlEditorToolsMenu = observer<Props>(function SqlEditorToolsMenu({ state, context, style }) {
const menu = useMenu({ menu: SQL_EDITOR_TOOLS_MENU, context });
menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state);
return (
<MenuBar style={[SQL_EDITOR_ACTIONS_MENU_STYLES, ...[style].flat(2)]} menu={menu} />
);
});
@@ -268,6 +268,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeQuery(): Promise<void> {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable) {
return;
}
const query = this.getSubQuery();
await this.executeQueryAction(
@@ -296,6 +302,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeQueryNewTab(): Promise<void> {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable) {
return;
}
const query = this.getSubQuery();
await this.executeQueryAction(
@@ -309,7 +321,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async showExecutionPlan(): Promise<void> {
if (!this.dialect?.supportsExplainExecutionPlan) {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable || !this.dialect?.supportsExplainExecutionPlan) {
return;
}
@@ -329,7 +344,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeScript(): Promise<void> {
if (this.isDisabled || this.isScriptEmpty) {
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isExecutable || this.isDisabled || this.isScriptEmpty) {
return;
}
@@ -43,7 +43,7 @@ const styles = css`
display: flex;
overflow: auto;
}
TabList:not([|script]) tab-outer:only-child {
TabList:not([|executable]) tab-outer:only-child {
display: none;
}
TextPlaceholder {
@@ -91,11 +91,22 @@ export const SqlResultTabs = observer<Props>(function SqlDataResult({ state, onT
}
}
function handleCanClose(tab: ITabData): boolean {
const resultTab = state.resultTabs.find(tabState => tabState.tabId === tab.tabId);
if (resultTab) {
const group = state.resultGroups.find(group => group.groupId === resultTab.groupId)!;
return dataSource?.databaseModels.some(model => model.id === group.modelId) !== true;
}
return true;
}
if (!state.tabs.length) {
return styled(style)(<TextPlaceholder>{translate(dataSource?.emptyPlaceholder ?? 'sql_editor_placeholder')}</TextPlaceholder>);
}
const script = dataSource?.hasFeature(ESqlDataSourceFeatures.script);
const executable = dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
const tabList = orderedTabs.map(tab => tab.id);
return styled(style)(
@@ -103,11 +114,12 @@ export const SqlResultTabs = observer<Props>(function SqlDataResult({ state, onT
<TabsState
currentTabId={state.currentTabId}
tabList={tabList}
canClose={handleCanClose}
enabledBaseActions
onChange={handleSelect}
onClose={handleClose}
>
<TabList aria-label='SQL Results' {...use({ script })} style={styles}>
<TabList aria-label='SQL Results' {...use({ executable })} style={styles}>
{orderedTabs.map(result => (
<SqlResultTab
key={result.id}
@@ -13,5 +13,6 @@ export const ACTION_SQL_EDITOR_EXECUTE_SCRIPT = createAction(
{
icon: '/icons/sql_script_exec.svg',
label: 'sql_editor_sql_execution_script_button_tooltip',
tooltip: 'sql_editor_sql_execution_script_button_tooltip',
}
);
@@ -20,6 +20,8 @@ export * from './SqlDataSource/MemorySqlDataSource';
export * from './SqlEditor/SQLCodeEditor/SQLCodeEditorLoader';
export * from './SqlEditor/ISQLEditorData';
export * from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA';
export * from './SqlEditor/SQL_EDITOR_ACTIONS_MENU';
export * from './SqlEditor/SQL_EDITOR_TOOLS_MENU';
export * from './SqlEditor/SQLEditorModeContext';
export * from './SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID';
export * from './SqlResultTabs/SqlResultTabsService';
@@ -32,7 +34,6 @@ export * from './SqlEditor';
export * from './SqlEditorModeService';
export * from './SqlEditorService';
export * from './SqlEditorSettingsService';
export * from './SqlEditor/SQL_EDITOR_ACTIONS_MENU';
export * from './SqlGenerators/SqlGeneratorsResource';
export * from './SqlGenerators/SqlGeneratorsBootstrap';