diff --git a/webapp/packages/core-utils/src/debounce.ts b/webapp/packages/core-utils/src/debounce.ts new file mode 100644 index 0000000000..27ccb358a5 --- /dev/null +++ b/webapp/packages/core-utils/src/debounce.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function debounce any>(func: T, delay: number) { + let timeout: ReturnType | undefined; + + return function (this: ThisParameterType, ...args: Parameters): any { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} \ No newline at end of file diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 2c9662e013..75bf76a083 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -37,4 +37,5 @@ export * from './getTextBetween'; export * from './timestampToDate'; export * from './bytesToSize'; export * from './isDefined'; -export * from './createPath'; \ No newline at end of file +export * from './createPath'; +export * from './debounce'; \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts index db99c503ec..b7d05f4900 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts @@ -59,9 +59,12 @@ export class PluginBootstrap extends Bootstrap { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); if (action === ACTION_SAVE) { - if (state.associatedScriptId) { + const currentTab = this.navigationTabsService.currentTab; + const resourceTabState = this.sqlEditorTabResourceService.state.get(currentTab?.id ?? ''); + + if (resourceTabState) { try { - await this.navResourceNodeService.write(state.associatedScriptId, state.query); + await this.navResourceNodeService.write(resourceTabState.nodeId, state.query); this.notificationService.logSuccess({ title: 'plugin_resource_manager_update_script_success' }); } catch (exception) { this.notificationService.logException(exception as any, 'plugin_resource_manager_update_script_error'); @@ -82,7 +85,10 @@ export class PluginBootstrap extends Bootstrap { await this.navTreeResource.preloadNodeParents(NodeManagerUtils.parentsFromPath(nodeId), nodeId); const node = await this.navNodeInfoResource.load(nodeId); - this.sqlEditorService.setAssociatedScriptId(node.id, state); + if (currentTab) { + this.sqlEditorTabResourceService.linkTab(currentTab.id, node.id); + } + this.sqlEditorService.setName(node.name ?? scriptName, state); this.notificationService.logSuccess({ title: 'plugin_resource_manager_save_script_success', message: node.name }); @@ -127,20 +133,23 @@ export class PluginBootstrap extends Bootstrap { return; } - const existingTab = this.sqlEditorTabService.sqlEditorTabs.find( - tab => tab.handlerState.associatedScriptId === data.nodeId - ); + const tabId = this.sqlEditorTabResourceService.getResourceTab(node.id); - if (existingTab) { - this.navigationTabsService.selectTab(existingTab.id); + if (tabId) { + this.navigationTabsService.selectTab(tabId); } else { - const value = await this.navResourceNodeService.read(data.nodeId); + const value = await this.navResourceNodeService.read(node.id); - await this.sqlEditorNavigatorService.openNewEditor({ + const contextProvider = await this.sqlEditorNavigatorService.openNewEditor({ name: node.name ?? 'Unknown script', query: value, - associatedScriptId: data.nodeId, }); + + const context = contextProvider.getContext(this.navigationTabsService.navigationTabContext); + + if (context.tab) { + this.sqlEditorTabResourceService.linkTab(context.tab.id, node.id); + } } } catch (exception) { this.notificationService.logException(exception as any, 'plugin_resource_manager_open_script_error'); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/SqlEditorTabResourceService.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/SqlEditorTabResourceService.ts index b7bb53e5b8..d2962a1fbd 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/SqlEditorTabResourceService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/SqlEditorTabResourceService.ts @@ -6,20 +6,34 @@ * you may not use this file except in compliance with the License. */ +import { makeObservable, observable } from 'mobx'; + import { ITab, NavigationTabsService, NavTreeResource, NodeManagerUtils } from '@cloudbeaver/core-app'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { WindowEventsService } from '@cloudbeaver/core-root'; import { ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; -import { throttleAsync } from '@cloudbeaver/core-utils'; +import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { debounce, throttleAsync } from '@cloudbeaver/core-utils'; import { NavResourceNodeService } from '@cloudbeaver/plugin-resource-manager'; -import { ISqlEditorTabState, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; +import { IQueryChangeData, ISqlEditorTabState, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab, SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; const SYNC_DELAY = 5 * 1000; +const VALUE_SYNC_DELAY = 0.5 * 1000; +const RESOURCE_TAB_STATE = 'sql_editor_resource_tab_state'; + +interface IResourceTabData { + nodeId: string; + parents: string[]; +} + +type ResourceTabState = Map; @injectable() export class SqlEditorTabResourceService { + state: ResourceTabState; + constructor( private readonly navigationTabsService: NavigationTabsService, private readonly sqlEditorService: SqlEditorService, @@ -27,28 +41,89 @@ export class SqlEditorTabResourceService { private readonly notificationService: NotificationService, private readonly navTreeResource: NavTreeResource, private readonly navResourceNodeService: NavResourceNodeService, - private readonly windowEventsService: WindowEventsService + private readonly windowEventsService: WindowEventsService, + private readonly localStorageSaveService: LocalStorageSaveService, ) { + this.state = new Map(); + this.onNodeDeleteHandler = this.onNodeDeleteHandler.bind(this); this.onTabSelectHandler = this.onTabSelectHandler.bind(this); + this.onTabCloseHandler = this.onTabCloseHandler.bind(this); this.onFocusChangeHandler = this.onFocusChangeHandler.bind(this); + this.onTabResourceValueChangeHandler = this.onTabResourceValueChangeHandler.bind(this); this.start = this.start.bind(this); this.stop = this.stop.bind(this); - this.syncTab = throttleAsync(this.syncTab, SYNC_DELAY); + this.updateTabQuery = throttleAsync(this.updateTabQuery, SYNC_DELAY); + this.updateResource = debounce(this.updateResource, VALUE_SYNC_DELAY); + + makeObservable(this, { + state: observable, + }); + + this.localStorageSaveService.withAutoSave(this.state, RESOURCE_TAB_STATE); } start() { this.navTreeResource.onItemDelete.addHandler(this.onNodeDeleteHandler); this.navigationTabsService.onTabSelect.addHandler(this.onTabSelectHandler); + this.navigationTabsService.onTabClose.addHandler(this.onTabCloseHandler); this.windowEventsService.onFocusChange.addHandler(this.onFocusChangeHandler); + this.sqlEditorService.onQueryChange.addHandler(this.onTabResourceValueChangeHandler); } stop() { this.navTreeResource.onItemDelete.removeHandler(this.onNodeDeleteHandler); this.navigationTabsService.onTabSelect.removeHandler(this.onTabSelectHandler); + this.navigationTabsService.onTabClose.removeHandler(this.onTabCloseHandler); this.windowEventsService.onFocusChange.removeHandler(this.onFocusChangeHandler); + this.sqlEditorService.onQueryChange.removeHandler(this.onTabResourceValueChangeHandler); + } + + linkTab(tabId: string, nodeId: string) { + const parents = NodeManagerUtils.parentsFromPath(nodeId); + + this.state.set(tabId, { + nodeId, + parents, + }); + } + + unlinkTab(tabId: string, closeTab = false) { + const state = this.state.get(tabId); + + if (state) { + this.state.delete(tabId); + if (closeTab) { + this.closeTab(tabId); + } + } + } + + getResourceTab(nodeId: string) { + for (const [tabId, data] of this.state) { + if (data.nodeId === nodeId) { + return tabId; + } + } + + return null; + } + + private onTabResourceValueChangeHandler(data: IQueryChangeData) { + if (data.prevQuery !== data.query) { + this.updateResource(data.query); + } + } + + private async updateResource(value: string) { + const currentTab = this.navigationTabsService.currentTab; + + if (currentTab && this.state.has(currentTab.id)) { + const state = this.state.get(currentTab.id)!; + await this.navResourceNodeService.write(state.nodeId, value); + } } private onFocusChangeHandler(focused: boolean) { @@ -59,39 +134,48 @@ export class SqlEditorTabResourceService { private onTabSelectHandler(tab: ITab) { if (isSQLEditorTab(tab)) { - this.syncTab(tab); + this.updateTabQuery(tab); + } + } + + private onTabCloseHandler(tab: ITab | undefined) { + if (tab && this.state.has(tab.id)) { + this.unlinkTab(tab.id); } } private onNodeDeleteHandler(keyObj: ResourceKey) { ResourceKeyUtils.forEach(keyObj, key => { - const tab = this.sqlEditorTabService.sqlEditorTabs.find(tab => tab.handlerState.associatedScriptId === key); - if (tab) { - this.navigationTabsService.closeTab(tab.id); + const tabId = this.getResourceTab(key); + if (tabId) { + this.closeTab(tabId); } }); } + private closeTab(tabId: string) { + this.navigationTabsService.closeTab(tabId); + } + private async syncCurrentTab() { const current = this.sqlEditorTabService.sqlEditorTabs.find( tab => tab.id === this.navigationTabsService.currentTabId ); if (current) { - await this.syncTab(current); + await this.updateTabQuery(current); } } - private async syncTab(tab: ITab) { + private async updateTabQuery(tab: ITab) { try { - if (!tab.handlerState.associatedScriptId) { + const state = this.state.get(tab.id); + + if (!state) { return; } - const parents = NodeManagerUtils.parentsFromPath(tab.handlerState.associatedScriptId); - const found = await this.navTreeResource.preloadNodeParents( - parents, tab.handlerState.associatedScriptId - ); + const found = await this.navTreeResource.preloadNodeParents(state.parents, state.nodeId); if (!found) { this.notificationService.logInfo({ @@ -100,11 +184,11 @@ export class SqlEditorTabResourceService { persistent: true, }); - this.sqlEditorService.setAssociatedScriptId('', tab.handlerState); + this.unlinkTab(tab.id); return; } - const query = await this.navResourceNodeService.read(tab.handlerState.associatedScriptId); + const query = await this.navResourceNodeService.read(state.nodeId); this.sqlEditorService.setQuery(query, tab.handlerState); } catch (exception) { this.notificationService.logException(exception as any, 'plugin_resource_manager_sync_script_error'); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts index 8a6f4c6e87..6d9c8c2d76 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts @@ -33,7 +33,6 @@ export interface ISQLEditorOptions { schemaId?: string; source?: string; query?: string; - associatedScriptId?: string; } export interface SQLCreateAction extends SQLEditorActionContext, ISQLEditorOptions { @@ -66,8 +65,8 @@ export class SqlEditorNavigatorService { .addHandler(this.navigateHandler.bind(this)); } - async openNewEditor(options: ISQLEditorOptions): Promise { - await this.navigator.execute({ + async openNewEditor(options: ISQLEditorOptions) { + return await this.navigator.execute({ type: SQLEditorNavigationAction.create, ...options, }); @@ -111,7 +110,6 @@ export class SqlEditorNavigatorService { data.name, data.source, data.query, - data.associatedScriptId ); if (tabOptions) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index cc69933a30..5424e1e5cb 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -94,14 +94,13 @@ export class SqlEditorTabService extends Bootstrap { name?: string, source?: string, query?: string, - associatedScriptId?: string ): ITabOptions | null { const order = this.getFreeEditorId(); return { handlerId: sqlEditorTabHandlerKey, - handlerState: this.sqlEditorService.getState(order, name, source, query, undefined, associatedScriptId), + handlerState: this.sqlEditorService.getState(order, name, source, query, undefined), }; } diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index 52bf033808..2a85f4cf5b 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -50,7 +50,6 @@ export interface ISqlEditorTabState { query: string; executionContext?: IConnectionExecutionContextInfo; currentTabId?: string; - associatedScriptId?: string; tabs: ISqlEditorResultTab[]; resultGroups: IResultGroup[]; resultTabs: IResultTab[]; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index c1ba5c7ca1..2c64c6053e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -66,7 +66,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, get activeSegmentMode(): ISQLEditorMode { - const contexts = this.onMode.execute(this); + const contexts = this.onMode.execute(this); const mode = contexts.getContext(SQLEditorModeContext); return mode; @@ -300,7 +300,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, setQuery(query: string): void { - this.state.query = query; + this.sqlEditorService.setQuery(query, this.state); this.parser.setScript(query); this.onUpdate.execute(); }, diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts index 4952870a2d..033c51e563 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -9,13 +9,22 @@ import { ConnectionExecutionContextResource, ConnectionExecutionContextService, ConnectionsManagerService, IConnectionExecutionContext, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { Executor } from '@cloudbeaver/core-executor'; import { GraphQLService, SqlCompletionProposal, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; import type { ISqlEditorTabState } from '@cloudbeaver/plugin-sql-editor'; export type SQLProposal = SqlCompletionProposal; +export interface IQueryChangeData { + prevQuery: string; + query: string; + state: ISqlEditorTabState; +} + @injectable() export class SqlEditorService { + readonly onQueryChange: Executor; + constructor( private readonly gql: GraphQLService, private readonly connectionsManagerService: ConnectionsManagerService, @@ -23,6 +32,7 @@ export class SqlEditorService { private readonly connectionExecutionContextService: ConnectionExecutionContextService, private readonly connectionExecutionContextResource: ConnectionExecutionContextResource ) { + this.onQueryChange = new Executor(); } getState( @@ -31,7 +41,6 @@ export class SqlEditorService { source?: string, query?: string, contextInfo?: IConnectionExecutionContextInfo, - associatedScriptId?: string, ): ISqlEditorTabState { return { name, @@ -46,7 +55,6 @@ export class SqlEditorService { statisticsTabs: [], currentModeId: '', - associatedScriptId: associatedScriptId ?? '', modeState: [], }; } @@ -97,16 +105,15 @@ export class SqlEditorService { return proposals as SQLProposal[]; } - setAssociatedScriptId(scriptId: string, state: ISqlEditorTabState) { - state.associatedScriptId = scriptId; - } - setName(name: string, state: ISqlEditorTabState) { state.name = name; } setQuery(query: string, state: ISqlEditorTabState) { + const prevQuery = state.query; + state.query = query; + this.onQueryChange.execute({ prevQuery, query, state }); } async resetExecutionContext(state: ISqlEditorTabState) { diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index 37c5ecf7a5..b365bc4749 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -23,6 +23,7 @@ import settingsMenu from '@cloudbeaver/plugin-settings-menu'; import settingsMenuAdministration from '@cloudbeaver/plugin-settings-menu-administration'; import sqlEditor from '@cloudbeaver/plugin-sql-editor'; import sqlEditorTab from '@cloudbeaver/plugin-sql-editor-navigation-tab'; +import sqlEditorResource from '@cloudbeaver/plugin-sql-editor-navigation-tab-resource'; import sqlEditorScreen from '@cloudbeaver/plugin-sql-editor-screen'; import theme from '@cloudbeaver/plugin-theme'; import userProfile from '@cloudbeaver/plugin-user-profile'; @@ -59,6 +60,7 @@ const PLUGINS: PluginManifest[] = [ settingsMenuAdministration, help, resourceManager, + sqlEditorResource, ]; bootstrap(PLUGINS);