Files
cloudbeaver/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts
T
Alexey 4550a71298 Refactor CB-3819 resources (#2031)
* CB-3819 refactor: split resources to modules

* CB-3819 refactor: move resource API to separate package

* CB-3819 refactor: move resource API to separate package

* CB-3819 refactor: split to submodules

* CB-3819 chore: update ts configuration

* CB-3819 refactor: move administration top bar to separate plugin

* CB-3819 feat: update tests

* CB-3819 chore: add pre scripts

* CB-3819 fix: prebuild tests-runner

* chore: restore cache for jest-tests github action

* chore: update configs

* chore: add frontend build action

* chore: rename build frontend github action

* chore: include test dependencies

* fix: default product build script

* chore: update github action

* chore: rename validation github action

* chore: update typescript cache key

* chore: update naming for github actions

* chore: run tests on ubuntu

* CB-3819 fix: pre build tasks

* CB-3819 fix: ignore order

* CB-3819 fix: remove prettier from gql sdk
2023-10-05 23:36:54 +03:00

522 lines
19 KiB
TypeScript

/*
* 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 { computed, makeObservable, observable, untracked } from 'mobx';
import { ConfirmationDialog } from '@cloudbeaver/core-blocks';
import {
ConnectionExecutionContextResource,
ConnectionExecutionContextService,
ConnectionInfoActiveProjectKey,
ConnectionInfoResource,
connectionProvider,
connectionSetter,
ConnectionsManagerService,
ContainerResource,
createConnectionParam,
ICatalogData,
IConnectionExecutorData,
IConnectionInfoParams,
objectCatalogProvider,
objectCatalogSetter,
objectSchemaProvider,
objectSchemaSetter,
} from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { NavNodeInfoResource, NodeManagerUtils, objectNavNodeProvider } from '@cloudbeaver/core-navigation-tree';
import { projectProvider, projectSetter, projectSetterState } from '@cloudbeaver/core-projects';
import { resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import type { NavNodeInfoFragment } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { ITab, ITabOptions, NavigationTabsService, TabHandler } from '@cloudbeaver/plugin-navigation-tabs';
import {
ESqlDataSourceFeatures,
ISQLDatasourceUpdateData,
ISqlEditorTabState,
SqlDataSourceService,
SqlEditorService,
SqlResultTabsService,
} from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab } from './isSQLEditorTab';
import { SqlEditorPanel } from './SqlEditorPanel';
import { SqlEditorTab } from './SqlEditorTab';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
@injectable()
export class SqlEditorTabService extends Bootstrap {
get sqlEditorTabs(): ITab<ISqlEditorTabState>[] {
return Array.from(this.navigationTabsService.findTabs<ISqlEditorTabState>(isSQLEditorTab));
}
readonly tabHandler: TabHandler<ISqlEditorTabState>;
readonly onCanClose: Executor<ITab<ISqlEditorTabState>>;
constructor(
private readonly navigationTabsService: NavigationTabsService,
private readonly notificationService: NotificationService,
private readonly sqlEditorService: SqlEditorService,
private readonly sqlResultTabsService: SqlResultTabsService,
private readonly connectionExecutionContextService: ConnectionExecutionContextService,
private readonly connectionExecutionContextResource: ConnectionExecutionContextResource,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly containerResource: ContainerResource,
private readonly commonDialogService: CommonDialogService,
) {
super();
this.onCanClose = new Executor();
this.tabHandler = this.navigationTabsService.registerTabHandler<ISqlEditorTabState>({
key: sqlEditorTabHandlerKey,
getTabComponent: () => SqlEditorTab,
getPanelComponent: () => SqlEditorPanel,
onRestore: this.handleTabRestore.bind(this),
onUnload: this.handleTabUnload.bind(this),
onClose: this.handleTabClose.bind(this),
onCloseSilent: this.handleTabCloseSilent.bind(this),
canClose: this.handleCanTabClose.bind(this),
extensions: [
objectNavNodeProvider(this.getNavNode.bind(this)),
projectSetterState(this.getProjectSetState.bind(this)),
projectProvider(this.getProjectId.bind(this)),
connectionProvider(this.getConnectionId.bind(this)),
objectCatalogProvider(this.getObjectCatalogId.bind(this)),
objectSchemaProvider(this.getObjectSchemaId.bind(this)),
projectSetter(this.setProjectId.bind(this)),
connectionSetter((connectionId, tab) => this.setConnectionId(tab, connectionId)),
objectCatalogSetter(this.setObjectCatalogId.bind(this)),
objectSchemaSetter(this.setObjectSchemaId.bind(this)),
],
});
makeObservable(this, {
sqlEditorTabs: computed<ITab<ISqlEditorTabState>[]>({
equals: (a, b) => isArraysEqual(a, b),
}),
});
}
register(): void {
this.sqlDataSourceService.onUpdate.addHandler(this.syncDatasourceUpdate.bind(this));
this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this));
this.connectionInfoResource.onItemDelete.addHandler(this.handleConnectionDelete.bind(this));
this.connectionExecutionContextResource.onItemUpdate.addHandler(this.handleExecutionContextUpdate.bind(this));
this.connectionExecutionContextResource.onItemDelete.addHandler(this.handleExecutionContextDelete.bind(this));
}
load(): void {}
createNewEditor(editorId: string, dataSourceKey: string, name?: string, source?: string, script?: string): ITabOptions<ISqlEditorTabState> | null {
const order = this.getFreeEditorId();
const handlerState = this.sqlEditorService.getState(editorId, dataSourceKey, order, source);
const datasource = this.sqlDataSourceService.create(handlerState, dataSourceKey, { name, script });
return {
id: editorId,
projectId: datasource.executionContext?.projectId ?? null,
handlerId: sqlEditorTabHandlerKey,
handlerState,
};
}
attachToProject(tab: ITab<ISqlEditorTabState>, projectId: string | null): void {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
projectId = dataSource?.projectId ?? projectId;
tab.projectId = projectId;
}
resetConnectionInfo(tab: ITab<ISqlEditorTabState>): void {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
dataSource?.setExecutionContext(undefined);
this.attachToProject(tab, null);
}
private async handleConnectionDelete(key: ResourceKeySimple<IConnectionInfoParams>) {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.executionContext;
}),
);
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (this.connectionInfoResource.isIntersect(key, contextConnection)) {
this.resetConnectionInfo(tab);
}
}
}
}
private getNavNode(tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (!executionContext) {
return;
}
const { projectId, connectionId, defaultCatalog, defaultSchema } = executionContext;
const connectionKey = createConnectionParam(projectId, connectionId);
const connection = this.connectionInfoResource.get(connectionKey);
if (!connection?.connected) {
return;
}
let catalogData: ICatalogData | undefined;
let schema: NavNodeInfoFragment | undefined;
if (defaultCatalog) {
catalogData = this.containerResource.getCatalogData(connectionKey, defaultCatalog);
if (catalogData && defaultSchema) {
schema = catalogData.schemaList.find(schema => schema.name === defaultSchema);
}
} else if (defaultSchema) {
schema = this.containerResource.getSchema(connectionKey, defaultSchema);
}
let nodeId = schema?.id ?? catalogData?.catalog.id;
if (!nodeId) {
nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connectionId);
}
const parents = this.navNodeInfoResource.getParents(nodeId);
untracked(() => this.navNodeInfoResource.load(nodeId!));
return {
nodeId,
path: parents,
};
}
private async handleExecutionContextUpdate(key: ResourceKeySimple<string>) {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.executionContext && ResourceKeyUtils.isIntersect(key, dataSource.executionContext.id);
}),
);
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId)!;
const executionContext = this.connectionExecutionContextService.get(dataSource.executionContext!.id)?.context;
if (!executionContext) {
const executionContext = dataSource.executionContext;
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
}
}
} else {
dataSource.setExecutionContext({ ...executionContext });
this.attachToProject(tab, executionContext.projectId);
}
}
}
private async handleExecutionContextDelete(key: ResourceKeySimple<string>) {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.executionContext;
}),
);
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId)!;
const executionContext = dataSource.executionContext;
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (ResourceKeyUtils.isIntersect(key, executionContext.id) && !this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
}
}
}
}
private getFreeEditorId() {
const ordered = this.sqlEditorTabs.map(tab => tab.handlerState.order);
return findMinimalFree(ordered, 1);
}
private async handleTabRestore(tab: ITab<ISqlEditorTabState>): Promise<boolean> {
if (
typeof tab.handlerState.editorId !== 'string' ||
typeof tab.handlerState.editorId !== 'string' ||
typeof tab.handlerState.order !== 'number' ||
!['string', 'undefined'].includes(typeof tab.handlerState.currentTabId) ||
!['string', 'undefined'].includes(typeof tab.handlerState.source) ||
!['string', 'undefined'].includes(typeof tab.handlerState.currentModeId) ||
!Array.isArray(tab.handlerState.modeState) ||
!Array.isArray(tab.handlerState.tabs) ||
!Array.isArray(tab.handlerState.executionPlanTabs) ||
!Array.isArray(tab.handlerState.resultGroups) ||
!Array.isArray(tab.handlerState.resultTabs) ||
!Array.isArray(tab.handlerState.statisticsTabs)
) {
await this.sqlDataSourceService.destroy(tab.handlerState.editorId);
return false;
}
const dataSource = this.sqlDataSourceService.create(tab.handlerState, tab.handlerState.datasourceKey);
const executionContext = dataSource.executionContext;
if (executionContext) {
await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey);
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
}
}
// clean old results
tab.handlerState.currentTabId = '';
tab.handlerState.tabs = observable([]);
tab.handlerState.resultGroups = observable([]);
tab.handlerState.resultTabs = observable([]);
tab.handlerState.executionPlanTabs = observable([]);
tab.handlerState.statisticsTabs = observable([]);
tab.handlerState.outputLogsTab = undefined;
return true;
}
private getProjectId(tab: ITab<ISqlEditorTabState>): string | undefined {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return dataSource?.projectId ?? undefined;
}
private getProjectSetState(tab: ITab<ISqlEditorTabState>): boolean {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.hasFeature(ESqlDataSourceFeatures.setProject);
}
private getConnectionId(tab: ITab<ISqlEditorTabState>): IConnectionInfoParams | undefined {
const context = this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext;
if (!context) {
return undefined;
}
return createConnectionParam(context.projectId, context.connectionId);
}
private getObjectCatalogId(tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const context = this.connectionExecutionContextResource.get(dataSource?.executionContext?.id ?? '');
return context?.defaultCatalog;
}
private getObjectSchemaId(tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const context = this.connectionExecutionContextResource.get(dataSource?.executionContext?.id ?? '');
return context?.defaultSchema;
}
private setProjectId(projectId: string | null, tab: ITab<ISqlEditorTabState>): boolean {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
dataSource?.setProject(projectId);
this.attachToProject(tab, projectId);
return true;
}
async setConnectionId(tab: ITab<ISqlEditorTabState>, connectionKey: IConnectionInfoParams, catalogId?: string, schemaId?: string) {
const state = await this.sqlEditorService.setConnection(tab.handlerState, connectionKey, catalogId, schemaId);
if (state) {
this.attachToProject(tab, connectionKey.projectId);
}
return state;
}
private async setObjectCatalogId(containerId: string, tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
if (!dataSource?.executionContext) {
return false;
}
const executionContext = this.connectionExecutionContextService.get(dataSource.executionContext.id);
if (!executionContext) {
return false;
}
try {
const context = await executionContext.update(containerId, dataSource.executionContext.defaultSchema);
dataSource.setExecutionContext({ ...context });
return true;
} catch (exception: any) {
this.notificationService.logException(exception, 'Failed to change SQL-editor catalog');
return false;
}
}
private async setObjectSchemaId(containerId: string, tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
if (!dataSource?.executionContext) {
return false;
}
const executionContext = this.connectionExecutionContextService.get(dataSource.executionContext.id);
if (!executionContext) {
return false;
}
try {
const context = await executionContext.update(dataSource.executionContext.defaultCatalog, containerId);
dataSource.setExecutionContext({ ...context });
return true;
} catch (exception: any) {
this.notificationService.logException(exception, 'Failed to change SQL-editor schema');
return false;
}
}
private async syncDatasourceUpdate(data: ISQLDatasourceUpdateData) {
const tab = this.sqlEditorTabs.find(tab => tab.handlerState.editorId === data.editorId);
if (tab) {
this.attachToProject(tab, data.datasource.projectId);
}
}
private async disconnectHandler(data: IConnectionExecutorData, contexts: IExecutionContextProvider<IConnectionExecutorData>) {
const connectionsKey = resourceKeyList(data.connections);
if (data.state === 'before') {
for (const tab of this.sqlEditorTabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (!executionContext) {
continue;
}
const connectionKey = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.isIntersect(connectionsKey, connectionKey)) {
continue;
}
const canDisconnect = await this.handleCanTabClose(tab);
if (!canDisconnect) {
ExecutorInterrupter.interrupt(contexts);
return;
}
}
}
}
private async handleCanTabClose(editorTab: ITab<ISqlEditorTabState>) {
const canCloseTabs = await this.sqlResultTabsService.canCloseResultTabs(editorTab.handlerState);
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();
}
}
const canDestroyDatasource = await this.sqlDataSourceService.canDestroy(editorTab.handlerState.editorId);
return canDestroyDatasource;
}
private async handleTabUnload(editorTab: ITab<ISqlEditorTabState>) {
await this.sqlDataSourceService.unload(editorTab.handlerState.editorId);
this.sqlResultTabsService.removeResultTabs(editorTab.handlerState);
}
private async handleTabCloseSilent(editorTab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
await this.sqlEditorService.destroyContext(dataSource.executionContext);
}
await this.sqlDataSourceService.destroySilent(editorTab.handlerState.editorId);
this.sqlResultTabsService.removeResultTabs(editorTab.handlerState);
}
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
await this.sqlEditorService.destroyContext(dataSource.executionContext);
}
await this.sqlDataSourceService.destroy(editorTab.handlerState.editorId);
}
}
function findMinimalFree(array: number[], base: number): number {
return array.sort((a, b) => b - a).reduceRight((prev, cur) => (prev === cur ? prev + 1 : prev), base);
}