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
@@ -11,7 +11,7 @@ import { computed, makeObservable } from 'mobx';
import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication';
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { Executor, ExecutorInterrupter, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root';
import { DataSynchronizationService, ServerConfigResource, ServerEventId } from '@cloudbeaver/core-root';
import { CachedMapAllKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { NavigationService } from '@cloudbeaver/core-ui';
import { isArraysEqual } from '@cloudbeaver/core-utils';
@@ -46,7 +46,7 @@ export class ProjectsService extends Dependency {
get activeProjects(): ProjectInfo[] {
let activeProjects: ProjectInfo[] = [];
if (activeProjects.length === 0 && this.activeProjectIds.length > 0) {
if (this.activeProjectIds.length > 0) {
activeProjects = this.projectInfoResource
.get(resourceKeyList(this.activeProjectIds))
.filter(Boolean) as ProjectInfo[];
@@ -79,6 +79,10 @@ export class ProjectsService extends Dependency {
}
get activeProjectIds(): string[] {
if (!this.serverConfigResource.distributed) {
return [];
}
return this.userProjectsSettings.activeProjectIds;
}
@@ -92,6 +96,7 @@ export class ProjectsService extends Dependency {
readonly getActiveProjectTask: ISyncExecutor;
constructor(
private readonly serverConfigResource: ServerConfigResource,
private readonly projectInfoResource: ProjectInfoResource,
private readonly userInfoResource: UserInfoResource,
private readonly userDataService: UserDataService,
+1 -1
View File
@@ -42,7 +42,7 @@ export const Tab = observer<TabProps>(function Tab({
const tab = useTab(tabId, onOpen, onClose, onClick);
const info = tab.getInfo();
const canClose = getComputed(() => !!onClose || tab.state.closable);
const canClose = getComputed(() => !!onClose || (tab.closable && tab.state.closable));
return styled(useStyles(BASE_TAB_STYLES, BASE_TAB_ACTION_STYLES, style))(
<TabContext.Provider value={tabContext}>
@@ -52,6 +52,9 @@ export function useTab(
get selected() {
return this.state.state.selectedId === this.tabId;
},
get closable() {
return this.state.canClose(this.tabId);
},
getInfo() {
return this.state.getTabInfo(this.tabId);
},
@@ -68,6 +71,7 @@ export function useTab(
},
}), {
selected: computed,
closable: computed,
state: observable.ref,
tabId: observable.ref,
}, {
@@ -87,8 +87,14 @@ export class TabsBootstrap extends Bootstrap {
this.menuService.addCreator({
isApplicable: context => {
const tab = context.tryGet(DATA_CONTEXT_TAB_ID);
const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT);
return !!state?.enabledBaseActions && context.get(DATA_CONTEXT_MENU) === MENU_TAB;
return (
!!tab
&& !!state?.enabledBaseActions
&& context.get(DATA_CONTEXT_MENU) === MENU_TAB
&& state.canClose(tab)
);
},
getItems: (context, items) => [
...items,
@@ -27,6 +27,7 @@ export interface ITabsContext<T = Record<string, any>> {
closable: boolean;
tabList?: string[];
enabledBaseActions?: boolean;
canClose: (tabId: string) => boolean;
getTabInfo: (tabId: string) => ITabInfo<T> | undefined;
getTabState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>) => T;
getLocalState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>) => T;
+14 -1
View File
@@ -32,6 +32,7 @@ type Props<T = Record<string, any>> = ExtractContainerProps<T> & React.PropsWith
autoSelect?: boolean;
tabList?: string[];
enabledBaseActions?: boolean;
canClose?: (tab: ITabData<T>) => boolean;
onChange?: (tab: ITabData<T>) => void;
onClose?: (tab: ITabData<T>) => void;
}>;
@@ -50,6 +51,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
enabledBaseActions,
onChange: onOpen,
onClose,
canClose,
...rest
}: Props<T>): React.ReactElement | null {
const props = useMemo(() => rest as any as T, [...Object.values(rest)]);
@@ -87,6 +89,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
const dynamic = useObjectRef(() => ({
selectedId: selectedId || currentTabId,
}), {
canClose,
open: onOpen,
close: onClose,
props,
@@ -176,8 +179,14 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
}, [!isNull(state.selectedId) && !isUndefined(state.selectedId)]);
const value = useObservableRef<ITabsContext<T>>(() => ({
canClose(tabId) {
return dynamic.canClose?.({
tabId,
props: dynamic.props,
}) ?? true;
},
getTabInfo(tabId: string) {
return dynamic.container?.getTabInfo(tabId);
return dynamic.container?.getTabInfo(tabId);
},
getTabState(tabId: string, valueGetter?: MetadataValueGetter<string, any>) {
return dynamic.container?.getTabState(
@@ -200,6 +209,10 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
});
},
async close(tabId: string) {
if (!this.canClose(tabId)) {
return;
}
await closeExecutor.execute({
tabId,
props: dynamic.props,
@@ -0,0 +1,13 @@
/*
* 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 { createAction } from '../createAction';
export const ACTION_EDIT = createAction('edit', {
label: 'core_view_action_edit_label',
});
+1
View File
@@ -1,5 +1,6 @@
export * from './Action/Actions/ACTION_COLLAPSE_ALL';
export * from './Action/Actions/ACTION_DELETE';
export * from './Action/Actions/ACTION_EDIT';
export * from './Action/Actions/ACTION_EXPORT';
export * from './Action/Actions/ACTION_FILTER';
export * from './Action/Actions/ACTION_LAYOUT';
@@ -3,4 +3,5 @@ export default [
['core_view_zoom_in', 'Zoom In'],
['core_view_zoom_out', 'Zoom out'],
['core_view_action_new_folder', 'New folder'],
['core_view_action_edit_label', 'Edit'],
];
@@ -3,4 +3,5 @@ export default [
['core_view_zoom_in', 'Zoom In'],
['core_view_zoom_out', 'Zoom out'],
['core_view_action_new_folder', 'New folder'],
['core_view_action_edit_label', 'Edit'],
];
@@ -3,4 +3,5 @@ export default [
['core_view_zoom_in', 'Приблизить'],
['core_view_zoom_out', 'Отдалить'],
['core_view_action_new_folder', 'Создать папку'],
['core_view_action_edit_label', 'Изменить'],
];
@@ -19,7 +19,7 @@ import { ActionService, ACTION_SAVE, DATA_CONTEXT_MENU, MenuService } from '@clo
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { NavResourceNodeService, ResourceManagerService, getResourceKeyFromNodeId } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerScriptsService, SaveScriptDialog, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, getSqlEditorName, ISqlDataSource, SqlDataSourceService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor';
import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, getSqlEditorName, ISqlDataSource, SqlDataSourceService, SqlEditorSettingsService, MemorySqlDataSource, LocalStorageSqlDataSource, SQL_EDITOR_TOOLS_MENU } from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { ResourceSqlDataSource } from './ResourceSqlDataSource';
@@ -66,7 +66,12 @@ export class PluginBootstrap extends Bootstrap {
return false;
}
return !(this.sqlDataSourceService.get(state.editorId) instanceof ResourceSqlDataSource);
const dataSource = this.sqlDataSourceService.get(state.editorId);
return (
dataSource instanceof MemorySqlDataSource
|| dataSource instanceof LocalStorageSqlDataSource
);
}
return false;
@@ -182,7 +187,7 @@ export class PluginBootstrap extends Bootstrap {
return (
this.resourceManagerService.enabled
&& context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_ACTIONS_MENU
&& context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_TOOLS_MENU
&& !!dataSource?.hasFeature(ESqlDataSourceFeatures.script)
);
},
@@ -89,14 +89,18 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
get features():ESqlDataSourceFeatures[] {
if (this.isReadonly()) {
return [ESqlDataSourceFeatures.script];
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable];
}
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName];
return [
ESqlDataSourceFeatures.script,
ESqlDataSourceFeatures.query,
ESqlDataSourceFeatures.executable,
ESqlDataSourceFeatures.setName,
];
}
private _script: string;
private saved: boolean;
private actions?: IResourceActions;
private info?: IResourceInfo;
private lastAction?: () => Promise<void>;
@@ -114,7 +118,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
super();
this.state = state;
this._script = '';
this.saved = true;
this.loaded = false;
this.resourceUseKeyId = null;
this.scheduler = new TaskScheduler(() => true);
@@ -207,7 +210,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
this._script = script;
super.setScript(script);
this.saved = false;
if (previous !== script) {
this.debouncedWrite();
@@ -347,6 +349,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
this.markUpdated();
this.loaded = true;
super.setScript(this.script);
this.saved = true;
} catch (exception: any) {
this.exception = exception;
} finally {
@@ -377,6 +380,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
});
}
async save(): Promise<void> {
await this.write();
}
private async saveProperties() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey) {
@@ -27,6 +27,7 @@ import {
objectSchemaSetter,
} from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
@@ -61,7 +62,8 @@ export class SqlEditorTabService extends Bootstrap {
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly containerResource: ContainerResource
private readonly containerResource: ContainerResource,
private readonly commonDialogService: CommonDialogService,
) {
super();
@@ -498,10 +500,32 @@ export class SqlEditorTabService extends Bootstrap {
private async handleCanTabClose(editorTab: ITab<ISqlEditorTabState>) {
const canCloseTabs = await this.sqlResultTabsService.canCloseResultTabs(editorTab.handlerState);
if (canCloseTabs) {
const contexts = await this.onCanClose.execute(editorTab);
if (ExecutorInterrupter.isInterrupted(contexts)) {
if (!canCloseTabs) {
return false;
}
const contexts = await this.onCanClose.execute(editorTab);
if (ExecutorInterrupter.isInterrupted(contexts)) {
return false;
}
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.isSaved() === false) {
const result = await this.commonDialogService.open(ConfirmationDialog, {
title: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_title',
subTitle: dataSource.name ?? undefined,
message: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_message',
confirmActionText: 'ui_yes',
extraStatus: 'no',
});
if (result === DialogueStateResult.Rejected) {
return false;
} else if (result === DialogueStateResult.Resolved) {
await dataSource.save();
} else {
await dataSource.reset();
}
}
@@ -2,4 +2,6 @@ export default [
['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'],
];
@@ -2,4 +2,6 @@ export default [
['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'],
];
@@ -2,4 +2,6 @@ export default [
['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Открыть SQL Редактор'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Открыть SQL Редактор для {arg:connection}'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Сохранить изменения'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Вы хотите сохранить изменения?'],
];
@@ -2,4 +2,6 @@ export default [
['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'],
['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'],
['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'],
];
@@ -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';