Feat CB-3030 conflict resolving (#1814)

* CB-3030 feat: conflict resolving

* CB-3030 fix: display os specific key instead of mod

* CB-3030 fix: sql editor status

* CB-3030 fix: styles and data validation

* CB-3030 fix: sql editor styles

---------

Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Alexey
2023-07-17 22:10:15 +03:00
committed by GitHub
co-authored by dariamarutkina
parent fc592908c2
commit 78c19187de
60 changed files with 3507 additions and 3412 deletions
@@ -6,8 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import type { ISqlDataSourceHistoryState } from '@cloudbeaver/plugin-sql-editor';
export interface IResourceSqlDataSourceState {
resourceKey?: string;
script: string;
baseScript: string;
executionContext?: IConnectionExecutionContextInfo;
baseExecutionContext?: IConnectionExecutionContextInfo;
history: ISqlDataSourceHistoryState;
}
@@ -16,7 +16,7 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { createPath, getPathName } from '@cloudbeaver/core-utils';
import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, KEY_BINDING_SAVE, KeyBindingService, MenuService } from '@cloudbeaver/core-view';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
import { getResourceKeyFromNodeId } from '@cloudbeaver/plugin-navigation-tree-rm';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerScriptsService, SaveScriptDialog, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import {
@@ -28,7 +28,6 @@ import {
MemorySqlDataSource,
SQL_EDITOR_TOOLS_MENU,
SqlDataSourceService,
SqlEditorService,
SqlEditorSettingsService,
} from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
@@ -41,7 +40,6 @@ import { SqlEditorTabResourceService } from './SqlEditorTabResourceService';
export class PluginBootstrap extends Bootstrap {
constructor(
private readonly navNodeManagerService: NavNodeManagerService,
private readonly navResourceNodeService: NavResourceNodeService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly navigationTabsService: NavigationTabsService,
private readonly notificationService: NotificationService,
@@ -55,7 +53,6 @@ export class PluginBootstrap extends Bootstrap {
private readonly menuService: MenuService,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
private readonly sqlEditorService: SqlEditorService,
private readonly resourceManagerResource: ResourceManagerResource,
private readonly resourceManagerScriptsService: ResourceManagerScriptsService,
private readonly keyBindingService: KeyBindingService,
@@ -70,33 +67,24 @@ export class PluginBootstrap extends Bootstrap {
this.actionService.addHandler({
id: 'scripts-base-handler',
isActionApplicable: (context, action): boolean => {
if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) {
const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE);
const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE);
if (!state) {
return false;
}
if (!state) {
return false;
}
if (!this.projectsService.activeProjects.some(project => project.canEditResources)) {
return false;
}
if (!this.projectsService.activeProjects.some(project => project.canEditResources)) {
return false;
}
const dataSource = this.sqlDataSourceService.get(state.editorId);
const dataSource = this.sqlDataSourceService.get(state.editorId);
if (action === ACTION_SAVE_AS_SCRIPT) {
return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource;
}
if (action === ACTION_SAVE) {
return dataSource instanceof ResourceSqlDataSource;
}
if (action === ACTION_SAVE_AS_SCRIPT) {
return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource;
}
return false;
},
isHidden: (context, action) => {
if (action === ACTION_SAVE) {
return this.sqlEditorService.autoSave;
return dataSource?.isAutoSaveEnabled === false;
}
return false;
@@ -196,7 +184,7 @@ export class PluginBootstrap extends Bootstrap {
return true;
}
return source.isLoading() || source.isSaved();
return source.isLoading() || source.isSaved;
}
return false;
@@ -17,7 +17,7 @@ import { TaskScheduler } from '@cloudbeaver/core-executor';
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager';
import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { createPath, debounce, getPathName, getPathParent, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { debounce, getPathName, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
@@ -28,7 +28,7 @@ interface IResourceInfo {
}
interface IResourceActions {
rename(dataSource: ResourceSqlDataSource, key: string, newKey: string): Promise<string>;
rename(dataSource: ResourceSqlDataSource, key: string, name: string): Promise<string>;
read(dataSource: ResourceSqlDataSource, key: string): Promise<string>;
write(dataSource: ResourceSqlDataSource, key: string, value: string): Promise<void>;
getProperties(dataSource: ResourceSqlDataSource, key: string): Promise<IConnectionExecutionContextInfo | undefined>;
@@ -55,7 +55,15 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
get script(): string {
return this._script;
return this.state.script;
}
get baseScript(): string {
return this.state.baseScript;
}
get baseExecutionContext(): IConnectionExecutionContextInfo | undefined {
return this.state.baseExecutionContext;
}
get projectId(): string | null {
@@ -92,11 +100,14 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName];
}
private _script: string;
get isAutoSaveEnabled(): boolean {
return this.sqlEditorService.autoSave;
}
private actions?: IResourceActions;
private info?: IResourceInfo;
private lastAction: (() => Promise<void>) | undefined;
private readonly state: IResourceSqlDataSourceState;
private state!: IResourceSqlDataSourceState;
private loaded: boolean;
private readonly scheduler: TaskScheduler;
@@ -110,8 +121,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
state: IResourceSqlDataSourceState,
) {
super();
this.state = state;
this._script = '';
this.bindState(state);
this.lastAction = undefined;
this.loaded = false;
this.resourceUseKeyId = null;
@@ -122,18 +132,15 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
resourceManagerResource.onDataOutdated.addHandler(this.syncResource);
makeObservable<this, '_script' | 'lastAction' | 'loaded'>(this, {
makeObservable<this, 'lastAction' | 'loaded'>(this, {
script: computed,
executionContext: computed,
resourceKey: computed,
features: computed<ESqlDataSourceFeatures[]>({
equals: isArraysEqual,
}),
_script: observable,
lastAction: observable.ref,
loaded: observable,
setExecutionContext: action,
setScript: action,
setResourceKey: action,
});
}
@@ -154,6 +161,20 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
return this.scheduler.executing;
}
canRename(name: string | null): boolean {
if (this.isReadonly()) {
return false;
}
if (!name) {
return false;
}
name = name.trim();
return !!this.actions && !!this.resourceKey && name.length > 0;
}
setResourceKey(resourceKey: string | undefined): void {
if (this.state.resourceKey && this.resourceUseKeyId) {
this.resourceManagerResource.free(toJS(this.state.resourceKey), this.resourceUseKeyId);
@@ -161,7 +182,8 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
this.state.resourceKey = toJS(resourceKey);
this.reloadResource();
this.loaded = false;
this.markOutdated();
this.onUpdate.execute();
}
@@ -174,78 +196,45 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
setName(name: string | null): void {
this.rename(name);
name = name?.trim() ?? null;
if (!name || name === this.name) {
return;
}
const previousName = this.name;
super.setName(name);
this.rename(name).catch(() => {
super.setName(previousName);
});
}
setProject(projectId: string | null): void {
super.setProject(projectId);
}
canRename(name: string | null): boolean {
if (this.isReadonly()) {
return false;
}
if (!name) {
return false;
}
name = name.trim();
return !!this.actions && !!this.resourceKey && this.saved && name.length > 0;
}
setScript(script: string): void {
const previous = this._script;
const previous = this.state.script;
if (previous === script) {
return;
}
this._script = script;
this.state.script = script;
super.setScript(script);
if (this.sqlEditorService.autoSave && previous !== script) {
if (this.isAutoSaveEnabled) {
this.debouncedWrite();
}
}
dispose(): void {
super.dispose();
this.resourceManagerResource.onItemUpdate.removeHandler(this.syncResource);
if (this.state.resourceKey && this.resourceUseKeyId) {
this.resourceManagerResource.free(this.state.resourceKey, this.resourceUseKeyId);
this.resourceUseKeyId = null;
}
}
setExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
executionContext = JSON.parse(JSON.stringify(toJS(executionContext) ?? {}));
syncResource(key: ResourceKey<string>) {
if (isResourceAlias(key)) {
return;
}
const resourceKey = this.resourceKey;
if (resourceKey && ResourceKeyUtils.some(key, key => resourceKey.startsWith(key))) {
this.reloadResource();
}
}
reloadResource(): void {
this.markOutdated();
this.saved = true;
this.loaded = false;
}
async load(): Promise<void> {
if (this.state.resourceKey && !this.resourceUseKeyId) {
this.resourceUseKeyId = this.resourceManagerResource.use(this.state.resourceKey);
}
await this.read();
}
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void {
if (this.resourceKey && executionContext?.projectId && getRmResourceKey(this.resourceKey).projectId !== executionContext.projectId) {
throw new Error('Resource SQL Data Source and Execution context projects don\t match');
}
if (!isObjectsEqual(toJS(this.state.executionContext), toJS(executionContext))) {
if (!isObjectsEqual(toJS(this.state.executionContext), executionContext)) {
const initNew =
!isValuesEqual(executionContext?.connectionId, this.executionContext?.connectionId, undefined) ||
!isValuesEqual(executionContext?.defaultCatalog, this.executionContext?.defaultCatalog, undefined) ||
@@ -263,130 +252,182 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
}
this.state.executionContext = toJS(executionContext);
this.state.executionContext = executionContext;
super.setExecutionContext(executionContext);
if (this.sqlEditorService.autoSave) {
if (this.isAutoSaveEnabled) {
this.debouncedSaveProperties();
}
}
}
async rename(name: string | null) {
await this.write();
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || !this.projectId || !this.saved || !name?.trim()) {
return;
}
name = this.projectInfoResource.getNameWithExtension(this.projectId, SCRIPTS_TYPE_ID, name);
this.lastAction = this.rename.bind(this, name);
this.message = 'plugin_sql_editor_navigation_tab_script_state_renaming';
try {
this.exception = null;
this.setResourceKey(await this.actions.rename(this, this.resourceKey, createPath(getPathParent(this.resourceKey), name)));
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
async read() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || !this.isOutdated() || !this.saved) {
return;
}
this.lastAction = this.read.bind(this);
this.message = 'plugin_sql_editor_navigation_tab_script_state_reading';
try {
this.exception = null;
this._script = await this.actions.read(this, this.resourceKey);
const executionContext = await this.actions.getProperties(this, this.resourceKey);
runInAction(() => {
if (executionContext) {
this.setExecutionContext(executionContext);
} else {
this.setExecutionContext();
}
});
this.markUpdated();
this.loaded = true;
super.setScript(this.script);
this.saved = true;
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
async write() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || this.saved) {
return;
}
this.lastAction = this.write.bind(this);
this.message = 'plugin_sql_editor_navigation_tab_script_state_saving';
try {
this.exception = null;
await this.actions.write(this, this.resourceKey, this.script);
this.saved = true;
this.markUpdated();
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
async load(): Promise<void> {
if (this.state.resourceKey && !this.resourceUseKeyId) {
this.resourceUseKeyId = this.resourceManagerResource.use(this.state.resourceKey);
}
await this.read();
}
async save(): Promise<void> {
await this.write();
await this.saveProperties();
try {
await this.write();
await this.saveProperties();
super.save();
} catch (exception: any) {
this.exception = exception;
}
}
private async saveProperties() {
dispose(): void {
super.dispose();
this.resourceManagerResource.onItemUpdate.removeHandler(this.syncResource);
if (this.state.resourceKey && this.resourceUseKeyId) {
this.resourceManagerResource.free(this.state.resourceKey, this.resourceUseKeyId);
this.resourceUseKeyId = null;
}
}
bindState(state: IResourceSqlDataSourceState): void {
this.state = state;
this.outdated = true;
this.history.restore(state.history);
}
private syncResource(key: ResourceKey<string>) {
if (isResourceAlias(key)) {
return;
}
const resourceKey = this.resourceKey;
if (resourceKey && ResourceKeyUtils.some(key, key => resourceKey.startsWith(key))) {
this.markOutdated();
}
}
private async rename(name: string) {
await this.save();
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey) {
return;
}
this.lastAction = this.rename.bind(this, name);
try {
if (!this.isSaved) {
throw new Error('Please save changes before renaming');
}
this.exception = null;
this.message = 'plugin_sql_editor_navigation_tab_script_state_renaming';
this.setResourceKey(await this.actions.rename(this, this.resourceKey, name));
} catch (exception: any) {
this.exception = exception;
throw exception;
} finally {
this.message = undefined;
}
});
}
private async read() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || !this.isOutdated()) {
return;
}
this.lastAction = this.read.bind(this);
try {
this.exception = null;
await this.readData();
} catch (exception: any) {
this.exception = exception;
}
});
}
private async saveProperties() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || this.isExecutionContextSaved) {
return;
}
this.lastAction = this.saveProperties.bind(this);
this.message = 'plugin_sql_editor_navigation_tab_script_state_updating';
try {
this.exception = null;
if (!this.isReadonly()) {
this.message = 'plugin_sql_editor_navigation_tab_script_state_updating';
const executionContext = await this.actions.setProperties(this, this.resourceKey, this.executionContext);
if (executionContext) {
this.setExecutionContext(executionContext);
} else {
this.setExecutionContext();
}
this.setExecutionContext(executionContext);
this.setBaseExecutionContext(this.executionContext);
}
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
private async write() {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || this.isScriptSaved) {
return;
}
this.lastAction = this.write.bind(this);
try {
this.exception = null;
await this.readData();
if (!this.isIncomingChanges) {
this.message = 'plugin_sql_editor_navigation_tab_script_state_saving';
await this.actions.write(this, this.resourceKey, this.script);
this.setBaseScript(this.script);
}
} finally {
this.message = undefined;
}
});
}
private async readData() {
try {
if (!this.actions || !this.resourceKey) {
return;
}
this.message = 'plugin_sql_editor_navigation_tab_script_state_reading';
const script = await this.actions.read(this, this.resourceKey);
const executionContext = await this.actions.getProperties(this, this.resourceKey);
runInAction(() => {
if (!this.loaded) {
if (this.baseScript !== script) {
this.setScript(script);
}
if (!isObjectsEqual(toJS(this.baseExecutionContext), executionContext)) {
this.setExecutionContext(executionContext);
}
this.setBaseScript(script);
this.setBaseExecutionContext(executionContext);
} else {
this.setIncomingExecutionContext(executionContext);
this.setIncomingScript(script);
}
this.markUpdated();
this.loaded = true;
});
} finally {
this.message = undefined;
}
}
private debouncedWrite() {
this.write();
}
@@ -394,4 +435,12 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
private debouncedSaveProperties() {
this.saveProperties();
}
protected setBaseScript(script: string): void {
this.state.baseScript = script;
}
protected setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
this.state.baseExecutionContext = toJS(executionContext);
}
}
@@ -16,12 +16,12 @@ import { getRmResourceKey, IResourceManagerMoveData, ResourceManagerResource } f
import { NetworkStateService, WindowEventsService } from '@cloudbeaver/core-root';
import { CachedMapAllKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
import { throttle } from '@cloudbeaver/core-utils';
import { createPath, getPathParent, throttle } from '@cloudbeaver/core-utils';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerScriptsService } from '@cloudbeaver/plugin-resource-manager-scripts';
import { getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
import { ResourceManagerScriptsService, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { createSqlDataSourceHistoryInitialState, getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState';
@@ -70,17 +70,18 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
for (const [key, value] of Array.from(map.entries())) {
if (
!['string', 'undefined'].includes(typeof value.resourceKey) ||
!['undefined', 'object'].includes(typeof value.executionContext) ||
!['string', 'undefined'].includes(typeof value.executionContext?.connectionId) ||
!['string', 'undefined'].includes(typeof value.executionContext?.id) ||
!['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultCatalog) ||
!['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultSchema)
!['string'].includes(typeof value.script) ||
!['string', 'undefined'].includes(typeof value.baseScript) ||
isExecutionContextInfoValid(value.executionContext) ||
isExecutionContextInfoValid(value.baseExecutionContext)
) {
map.delete(key);
}
}
return map;
},
this.bindState.bind(this),
'indexed',
);
}
@@ -97,20 +98,16 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
this.connectionInfoResource,
this.resourceManagerResource,
this.sqlEditorService,
this.createState(editorId),
this.createState(editorId, options?.script),
);
if (options?.executionContext) {
dataSource.setExecutionContext(options.executionContext);
}
if (options?.script) {
dataSource.setScript(options.script);
}
if (options?.name) {
dataSource.setName(options.name);
}
// if (options?.name) {
// dataSource.setName(options.name);
// }
dataSource.setActions({
rename: this.rename.bind(this),
@@ -138,15 +135,11 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
},
onDestroy: (_, editorId) => this.deleteState(editorId),
onUnload: async dataSource => {
if (dataSource instanceof ResourceSqlDataSource) {
await dataSource.write();
}
await dataSource.save();
},
canDestroy: async (dataSource, editorId) => {
try {
if (dataSource instanceof ResourceSqlDataSource) {
await dataSource.write();
}
await dataSource.save();
} catch {
const tab = this.sqlEditorTabService.sqlEditorTabs.find(tab => tab.handlerState.editorId === editorId);
let name: string | undefined = undefined;
@@ -173,12 +166,15 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
load(): void | Promise<void> {}
private createState(editorId: string, resourceKey?: string): IResourceSqlDataSourceState {
private createState(editorId: string, script?: string, resourceKey?: string): IResourceSqlDataSourceState {
let state = this.dataSourceStateState.get(editorId);
if (!state) {
state = observable<IResourceSqlDataSourceState>({
script: script ?? '',
baseScript: script ?? '',
resourceKey,
history: createSqlDataSourceHistoryInitialState(script),
});
this.dataSourceStateState.set(editorId, state);
@@ -250,12 +246,18 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
this.navigationTabsService.closeTabSilent(resourceKeyList(tabs), true);
}
private async rename(dataSource: ResourceSqlDataSource, resourceKey: string, newResourceKey: string): Promise<string> {
private async rename(dataSource: ResourceSqlDataSource, resourceKey: string, name: string): Promise<string> {
if (!dataSource.projectId) {
throw new Error('Project ID is not defined');
}
if (!this.resourceManagerService.enabled) {
throw new Error('Resource Manager disabled');
}
try {
name = this.projectInfoResource.getNameWithExtension(dataSource.projectId, SCRIPTS_TYPE_ID, name);
const newResourceKey = createPath(getPathParent(resourceKey), name);
await this.navResourceNodeService.move(resourceKey, newResourceKey);
return newResourceKey;
} catch (exception) {
@@ -308,4 +310,22 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
throw exception;
}
}
private bindState() {
for (const [editorId, datasource] of this.sqlDataSourceService.dataSources) {
if (datasource instanceof ResourceSqlDataSource) {
datasource.bindState(this.createState(editorId));
}
}
}
}
function isExecutionContextInfoValid(executionContext: IConnectionExecutionContextInfo | undefined) {
return (
!['undefined', 'object'].includes(typeof executionContext) ||
!['string', 'undefined'].includes(typeof executionContext?.connectionId) ||
!['string', 'undefined'].includes(typeof executionContext?.id) ||
!['string', 'undefined', 'object'].includes(typeof executionContext?.defaultCatalog) ||
!['string', 'undefined', 'object'].includes(typeof executionContext?.defaultSchema)
);
}