diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
index eee3bae054..0c4f02e26c 100644
--- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
@@ -132,6 +132,12 @@ public WebSession(
this.lastAccessTime = this.createTime;
setLocale(CommonUtils.toString(httpSession.getAttribute(ATTR_LOCALE), this.locale));
this.sessionHandlers = sessionHandlers;
+ //force authorization of anonymous session to avoid access error,
+ //because before authorization could be called by any request,
+ //but now 'updateInfo' is called only in special requests,
+ //and the order of requests is not guaranteed.
+ //look at CB-4747
+ refreshSessionAuth();
}
@Override
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
index 295a0c5734..6704c92ffd 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
@@ -131,7 +131,6 @@ public WebSession getWebSession(
log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'");
webSession.setCacheExpired(!httpSession.isNew());
- webSession.updateInfo(request, response);
sessionMap.put(sessionId, webSession);
} else {
diff --git a/server/drivers/postgresql/pom.xml b/server/drivers/postgresql/pom.xml
index 2efa74ee8a..a0e12cbf1c 100644
--- a/server/drivers/postgresql/pom.xml
+++ b/server/drivers/postgresql/pom.xml
@@ -18,7 +18,7 @@
org.postgresql
postgresql
- 42.5.2
+ 42.7.2
net.postgis
diff --git a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts
index 1aeebf887c..dc8d72f870 100644
--- a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts
+++ b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts
@@ -6,10 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import type { EditorView } from '@codemirror/view';
-import type { SelectionRange } from '@codemirror/state';
export interface IEditorRef {
container: HTMLDivElement | null;
view: EditorView | null;
- selection: SelectionRange | null;
}
diff --git a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts
index 930730697f..519d3aed3b 100644
--- a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts
+++ b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts
@@ -9,7 +9,7 @@ import type { Compartment, Extension, SelectionRange } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
/** Currently we support only main selection range */
-interface ISelection {
+export interface ISelection {
anchor: number;
head?: number;
}
diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx
index 6fe6a16eaf..985d973804 100644
--- a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx
+++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { MergeView } from '@codemirror/merge';
-import { Annotation, Compartment, Extension, StateEffect, TransactionSpec } from '@codemirror/state';
+import { Annotation, Compartment, EditorState, Extension, StateEffect, TransactionSpec } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { observer } from 'mobx-react-lite';
import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -17,6 +17,7 @@ import type { IEditorRef } from './IEditorRef';
import type { IReactCodeMirrorProps } from './IReactCodemirrorProps';
import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext';
import { useCodemirrorExtensions } from './useCodemirrorExtensions';
+import { validateCursorBoundaries } from './validateCursorBoundaries';
const External = Annotation.define();
@@ -55,15 +56,11 @@ export const ReactCodemirror = observer(
const [view, setView] = useState(null);
const [incomingView, setIncomingView] = useState(null);
const callbackRef = useObjectRef({ onChange, onCursorChange, onUpdate });
- const [selection, setSelection] = useState(view?.state.selection.main ?? null);
useLayoutEffect(() => {
if (container) {
const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
const remote = update.transactions.some(tr => tr.annotation(External));
- if (update.selectionSet) {
- setSelection(update.state.selection.main);
- }
if (update.docChanged && !remote) {
const doc = update.state.doc;
@@ -89,9 +86,15 @@ export const ReactCodemirror = observer(
effects.push(compartment.of(extension));
}
+ const tempState = EditorState.create({
+ doc: value,
+ });
+
if (incomingValue !== undefined) {
merge = new MergeView({
a: {
+ doc: value,
+ selection: cursor && validateCursorBoundaries(cursor, tempState.doc.length),
extensions: [updateListener, ...effects],
},
b: {
@@ -104,11 +107,19 @@ export const ReactCodemirror = observer(
incomingView = merge.b;
} else {
editorView = new EditorView({
+ state: EditorState.create({
+ doc: value,
+ selection: cursor && validateCursorBoundaries(cursor, tempState.doc.length),
+ extensions: [updateListener, ...effects],
+ }),
parent: container,
- extensions: [updateListener, ...effects],
});
}
+ editorView.dispatch({
+ scrollIntoView: true,
+ });
+
if (incomingView) {
setIncomingView(incomingView);
}
@@ -168,9 +179,13 @@ export const ReactCodemirror = observer(
let isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < view.state.doc.length;
- if (value !== undefined && value !== view.state.doc.toString()) {
- transaction.changes = { from: 0, to: view.state.doc.length, insert: value };
- isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < value.length;
+ if (value !== undefined) {
+ const newText = view.state.toText(value);
+
+ if (!newText.eq(view.state.doc)) {
+ transaction.changes = { from: 0, to: view.state.doc.length, insert: newText };
+ isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < newText.length;
+ }
}
if (cursor && isCursorInDoc && (view.state.selection.main.anchor !== cursor.anchor || view.state.selection.main.head !== cursor.head)) {
@@ -184,10 +199,14 @@ export const ReactCodemirror = observer(
});
useLayoutEffect(() => {
- if (incomingValue !== undefined && incomingView && incomingValue !== incomingView.state.doc.toString()) {
- incomingView.dispatch({
- changes: { from: 0, to: incomingView.state.doc.length, insert: incomingValue },
- });
+ if (incomingValue !== undefined && incomingView) {
+ const newValue = incomingView.state.toText(incomingValue);
+
+ if (!newValue.eq(incomingView.state.doc)) {
+ incomingView.dispatch({
+ changes: { from: 0, to: incomingView.state.doc.length, insert: newValue },
+ });
+ }
}
}, [incomingValue, incomingView]);
@@ -202,9 +221,8 @@ export const ReactCodemirror = observer(
() => ({
container,
view,
- selection,
}),
- [container, view, selection],
+ [container, view],
);
const context = useMemo(
diff --git a/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts b/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts
new file mode 100644
index 0000000000..352a0b08d0
--- /dev/null
+++ b/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts
@@ -0,0 +1,15 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2024 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 type { ISelection } from './IReactCodemirrorProps';
+
+export function validateCursorBoundaries(selection: ISelection, documentLength: number): ISelection {
+ return {
+ anchor: Math.min(selection.anchor, documentLength),
+ head: selection.head === undefined ? undefined : Math.min(selection.head, documentLength),
+ };
+}
diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts
index 27b95e75c3..c72a5278c4 100644
--- a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts
+++ b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts
@@ -7,19 +7,33 @@
*/
import type { Connection } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
+import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root';
import { DataViewerSettingsService } from './DataViewerSettingsService';
@injectable()
export class DataViewerService {
get canCopyData() {
+ if (this.sessionPermissionsResource.has(EAdminPermission.admin)) {
+ return true;
+ }
+
return !this.dataViewerSettingsService.settings.getValue('disableCopyData');
}
- constructor(private readonly dataViewerSettingsService: DataViewerSettingsService) {}
+ constructor(
+ private readonly dataViewerSettingsService: DataViewerSettingsService,
+ private readonly sessionPermissionsResource: SessionPermissionsResource,
+ ) {}
isDataEditable(connection: Connection) {
+ if (connection.readOnly) {
+ return false;
+ }
+
+ const isAdmin = this.sessionPermissionsResource.has(EAdminPermission.admin);
const disabled = this.dataViewerSettingsService.settings.getValue('disableEdit');
- return !disabled && !connection.readOnly;
+
+ return isAdmin || !disabled;
}
}
diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts
index 6831fdab89..afeb6a8def 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/en.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts
@@ -50,9 +50,9 @@ export default [
['data_viewer_model_not_loaded', 'Table model is not loaded'],
['settings_data_editor', 'Data Editor'],
['settings_data_editor_disable_edit_name', 'Disable Edit'],
- ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'],
+ ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer for non-admin users'],
['settings_data_editor_disable_data_copy_name', 'Disable Copy'],
- ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer'],
+ ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer for non-admin users'],
['settings_data_editor_fetch_min_name', 'Minimum fetch size'],
['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'],
['settings_data_editor_fetch_max_name', 'Maximum fetch size'],
diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts
index 35306f85a3..a35769e675 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/it.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts
@@ -43,9 +43,9 @@ export default [
['data_viewer_model_not_loaded', 'Table model is not loaded'],
['settings_data_editor', 'Data Editor'],
['settings_data_editor_disable_edit_name', 'Disable Edit'],
- ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'],
+ ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer for non-admin users'],
['settings_data_editor_disable_data_copy_name', 'Disable Copy'],
- ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer'],
+ ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer for non-admin users'],
['settings_data_editor_fetch_min_name', 'Minimum fetch size'],
['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'],
['settings_data_editor_fetch_max_name', 'Maximum fetch size'],
diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts
index 05dd579c74..307b45cd6b 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts
@@ -44,9 +44,9 @@ export default [
['data_viewer_model_not_loaded', 'Не удалось загрузить модель таблицы'],
['settings_data_editor', 'Редактор данных'],
['settings_data_editor_disable_edit_name', 'Отключить редактирование'],
- ['settings_data_editor_disable_edit_description', 'Отключить редактирование данных'],
+ ['settings_data_editor_disable_edit_description', 'Отключить редактирование данных для пользователей без прав администратора'],
['settings_data_editor_disable_data_copy_name', 'Отключить копирование'],
- ['settings_data_editor_disable_data_copy_description', 'Отключить копирование данных'],
+ ['settings_data_editor_disable_data_copy_description', 'Отключить копирование данных для пользователей без прав администратора'],
['settings_data_editor_fetch_min_name', 'Минимальный размер выборки'],
['settings_data_editor_fetch_min_description', 'Минимальное количество строк для выборки'],
['settings_data_editor_fetch_max_name', 'Максимальный размер выборки'],
diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts
index 4c0bba6c65..d5bf26d577 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts
@@ -50,9 +50,9 @@ export default [
['data_viewer_model_not_loaded', 'Table model is not loaded'],
['settings_data_editor', 'Data Editor'],
['settings_data_editor_disable_edit_name', 'Disable Edit'],
- ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'],
+ ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer for non-admin users'],
['settings_data_editor_disable_data_copy_name', 'Disable Copy'],
- ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer'],
+ ['settings_data_editor_disable_data_copy_description', 'Disable copying of data in Data Viewer for non-admin users'],
['settings_data_editor_fetch_min_name', 'Minimum fetch size'],
['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'],
['settings_data_editor_fetch_max_name', 'Maximum fetch size'],
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 81c06f261c..b1e9c41be9 100644
--- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts
+++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts
@@ -7,7 +7,7 @@
*/
import { computed, makeObservable, observable, untracked } from 'mobx';
-import { ConfirmationDialog } from '@cloudbeaver/core-blocks';
+import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks';
import {
ConnectionExecutionContextResource,
ConnectionExecutionContextService,
@@ -40,16 +40,18 @@ import {
ESqlDataSourceFeatures,
ISQLDatasourceUpdateData,
ISqlEditorTabState,
+ SQL_EDITOR_TAB_STATE_SCHEMA,
SqlDataSourceService,
SqlEditorService,
SqlResultTabsService,
} from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab } from './isSQLEditorTab';
-import { SqlEditorPanel } from './SqlEditorPanel';
-import { SqlEditorTab } from './SqlEditorTab';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
+const SqlEditorPanel = importLazyComponent(() => import('./SqlEditorPanel').then(m => m.SqlEditorPanel));
+const SqlEditorTab = importLazyComponent(() => import('./SqlEditorTab').then(m => m.SqlEditorTab));
+
@injectable()
export class SqlEditorTabService extends Bootstrap {
get sqlEditorTabs(): ITab[] {
@@ -273,20 +275,7 @@ export class SqlEditorTabService extends Bootstrap {
}
private async handleTabRestore(tab: ITab): Promise {
- 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)
- ) {
+ if (!SQL_EDITOR_TAB_STATE_SCHEMA.safeParse(tab.handlerState).success) {
await this.sqlDataSourceService.destroy(tab.handlerState.editorId);
return false;
}
diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx
index dda25ca030..ce4ff3f9d6 100644
--- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx
+++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx
@@ -6,13 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
import { MenuBarSmallItem, useExecutor, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { DATA_CONTEXT_NAV_NODE, getNodesFromContext, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
-import { TabContainerPanelComponent, useDNDBox, useTabLocalState } from '@cloudbeaver/core-ui';
+import { TabContainerPanelComponent, useDNDBox } from '@cloudbeaver/core-ui';
import { closeCompletion, IEditorRef, Prec, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
import type { ISqlEditorModeProps } from '@cloudbeaver/plugin-sql-editor';
@@ -26,36 +26,16 @@ import style from './SQLCodeEditorPanel.m.css';
import { SqlEditorInfoBar } from './SqlEditorInfoBar';
import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel';
-interface ILocalSQLCodeEditorPanelState {
- selection: { from: number; to: number };
-}
-
export const SQLCodeEditorPanel: TabContainerPanelComponent = observer(function SQLCodeEditorPanel({ data }) {
const notificationService = useService(NotificationService);
const navNodeManagerService = useService(NavNodeManagerService);
const translate = useTranslate();
- const localState = useTabLocalState(() => ({ selection: { from: 0, to: 0 } }));
const styles = useS(style);
const [editorRef, setEditorRef] = useState(null);
const editor = useSQLCodeEditor(editorRef);
- useEffect(() => {
- editorRef?.view?.dispatch({
- selection: { anchor: Math.min(localState.selection.to, data.value.length), head: Math.min(localState.selection.to, data.value.length) },
- scrollIntoView: true,
- });
- }, [editorRef?.view, localState, data]);
-
- useEffect(() => {
- if (!editorRef?.selection) {
- return;
- }
-
- localState.selection = { ...editorRef?.selection };
- }, [editorRef?.selection]);
-
const panel = useSQLCodeEditorPanel(data, editor);
const extensions = useCodemirrorExtensions(undefined, [ACTIVE_QUERY_EXTENSION, Prec.lowest(QUERY_STATUS_GUTTER_EXTENSION)]);
const autocompletion = useSqlDialectAutocompletion(data);
diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts
index 270e4ca7d3..fe3a087017 100644
--- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts
+++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts
@@ -5,15 +5,11 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
-import React from 'react';
-
+import { importLazyComponent } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';
import { ESqlDataSourceFeatures, SqlEditorModeService } from '@cloudbeaver/plugin-sql-editor';
-const SQLCodeEditorPanel = React.lazy(async () => {
- const { SQLCodeEditorPanel } = await import('./SQLCodeEditorPanel');
- return { default: SQLCodeEditorPanel };
-});
+const SQLCodeEditorPanel = importLazyComponent(() => import('./SQLCodeEditorPanel').then(module => module.SQLCodeEditorPanel));
@injectable()
export class SQLCodeEditorPanelService {
diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts
index 43ff3ac336..313ad44b18 100644
--- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts
+++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts
@@ -10,7 +10,6 @@ import { useCallback } from 'react';
import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
import { throttle } from '@cloudbeaver/core-utils';
-import type { Transaction, ViewUpdate } from '@cloudbeaver/plugin-codemirror6';
import type { ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';
import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor';
diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
index ccbc1f20a3..a89184dbe7 100644
--- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
+++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts
@@ -5,63 +5,75 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
-import type { IOutputLogType } from './SqlResultTabs/OutputLogs/IOutputLogTypes';
+import { schema } from '@cloudbeaver/core-utils';
-export interface IResultTab {
- tabId: string;
- // when query return several results they all have one groupId
- // new group id generates every time you execute query in new tab
- groupId: string;
- indexInResultSet: number;
-}
+import { OUTPUT_LOG_TYPES } from './SqlResultTabs/OutputLogs/IOutputLogTypes';
-export interface IStatisticsTab {
- tabId: string;
- order: number;
-}
+export const RESULT_TAB_SCHEMA = schema.object({
+ tabId: schema.string(),
+ groupId: schema.string(),
+ indexInResultSet: schema.number(),
+ presentationId: schema.string(),
+ valuePresentationId: schema.nullable(schema.string()),
+});
-export interface IResultGroup {
- groupId: string;
- modelId: string;
- order: number;
- nameOrder: number;
- query: string;
-}
+export type IResultTab = schema.infer;
-export interface ISqlEditorResultTab {
- id: string;
- order: number;
- name: string;
- icon: string;
-}
+export const STATISTIC_TAB_SCHEMA = schema.object({
+ tabId: schema.string(),
+ order: schema.number(),
+});
-export interface IExecutionPlanTab {
- tabId: string;
- order: number;
- query: string;
- options?: Record;
-}
+export type IStatisticsTab = schema.infer;
-export interface IOutputLogsTab extends ISqlEditorResultTab {
- selectedLogTypes: IOutputLogType[];
-}
+export const RESULT_GROUP_SCHEMA = schema.object({
+ groupId: schema.string(),
+ modelId: schema.string(),
+ order: schema.number(),
+ nameOrder: schema.number(),
+ query: schema.string(),
+});
-export interface ISqlEditorTabState {
- editorId: string;
- datasourceKey: string;
+export type IResultGroup = schema.infer;
- source?: string;
- order: number;
+export const SQL_EDITOR_RESULT_TAB_SCHEMA = schema.object({
+ id: schema.string(),
+ order: schema.number(),
+ name: schema.string(),
+ icon: schema.string(),
+});
- currentTabId?: string;
- tabs: ISqlEditorResultTab[];
- resultGroups: IResultGroup[];
- resultTabs: IResultTab[];
- statisticsTabs: IStatisticsTab[];
- executionPlanTabs: IExecutionPlanTab[];
- outputLogsTab?: IOutputLogsTab;
+export type ISqlEditorResultTab = schema.infer;
- // mode
- currentModeId?: string;
- modeState: Array<[string, any]>;
-}
+export const EXECUTION_PLAN_TAB_SCHEMA = schema.object({
+ tabId: schema.string(),
+ order: schema.number(),
+ query: schema.string(),
+ options: schema.record(schema.any()).optional(),
+});
+
+export type IExecutionPlanTab = schema.infer;
+
+const OUTPUT_LOGS_TAB_SCHEMA = SQL_EDITOR_RESULT_TAB_SCHEMA.extend({
+ selectedLogTypes: schema.array(schema.enum(OUTPUT_LOG_TYPES)),
+});
+
+export type IOutputLogsTab = schema.infer;
+
+export const SQL_EDITOR_TAB_STATE_SCHEMA = schema.object({
+ editorId: schema.string(),
+ datasourceKey: schema.string(),
+ source: schema.string().optional(),
+ order: schema.number(),
+ currentTabId: schema.string().optional(),
+ tabs: schema.array(SQL_EDITOR_RESULT_TAB_SCHEMA),
+ resultGroups: schema.array(RESULT_GROUP_SCHEMA),
+ resultTabs: schema.array(RESULT_TAB_SCHEMA),
+ statisticsTabs: schema.array(STATISTIC_TAB_SCHEMA),
+ executionPlanTabs: schema.array(EXECUTION_PLAN_TAB_SCHEMA),
+ outputLogsTab: OUTPUT_LOGS_TAB_SCHEMA.optional(),
+ currentModeId: schema.string().optional(),
+ modeState: schema.array(schema.tuple([schema.string(), schema.any()])),
+});
+
+export type ISqlEditorTabState = schema.infer;
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
index 763323df8b..bcf54266d4 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
@@ -14,7 +14,7 @@ import type { IDatabaseDataModel, IDatabaseResultSet } from '@cloudbeaver/plugin
import type { IDataQueryOptions } from '../QueryDataSource';
import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures';
-import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey } from './ISqlDataSource';
+import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey, ISqlEditorCursor } from './ISqlDataSource';
import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory';
import { SqlDataSourceHistory } from './SqlDataSourceHistory/SqlDataSourceHistory';
@@ -37,6 +37,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
incomingExecutionContext: IConnectionExecutionContextInfo | undefined | null;
exception?: Error | Error[] | null | undefined;
+ get cursor(): ISqlEditorCursor {
+ return this.innerCursorState;
+ }
+
get isIncomingChanges(): boolean {
return this.incomingScript !== undefined || this.incomingExecutionContext !== null;
}
@@ -81,6 +85,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
protected outdated: boolean;
protected editing: boolean;
+ protected innerCursorState: ISqlEditorCursor;
constructor(icon = '/icons/sql_script_m.svg') {
this.icon = icon;
@@ -91,6 +96,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.message = undefined;
this.outdated = true;
this.editing = true;
+ this.innerCursorState = { begin: 0, end: 0 };
this.history = new SqlDataSourceHistory();
this.onUpdate = new SyncExecutor();
this.onSetScript = new SyncExecutor();
@@ -107,7 +113,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY));
- makeObservable(this, {
+ makeObservable(this, {
isSaved: computed,
isIncomingChanges: computed,
isAutoSaveEnabled: computed,
@@ -130,6 +136,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
outdated: observable.ref,
message: observable.ref,
editing: observable.ref,
+ innerCursorState: observable.ref,
incomingScript: observable.ref,
incomingExecutionContext: observable.ref,
});
@@ -225,6 +232,20 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
return this.features.includes(feature);
}
+ setCursor(begin: number, end = begin): void {
+ if (begin > end) {
+ throw new Error('Cursor begin can not be greater than the end of it');
+ }
+
+ const scriptLength = this.script.length;
+
+ this.innerCursorState = Object.freeze({
+ begin: Math.min(begin, scriptLength),
+ end: Math.min(end, scriptLength),
+ });
+ this.onUpdate.execute();
+ }
+
setEditing(state: boolean): void {
this.editing = state;
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
index 555c23b736..efa60817e7 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
@@ -23,6 +23,11 @@ export interface ISetScriptData {
source?: string;
}
+export interface ISqlEditorCursor {
+ readonly begin: number;
+ readonly end: number;
+}
+
export interface ISqlDataSource extends ILoadableState {
readonly name: string | null;
readonly icon?: string;
@@ -33,6 +38,7 @@ export interface ISqlDataSource extends ILoadableState {
readonly projectId: string | null;
readonly script: string;
+ readonly cursor: ISqlEditorCursor;
readonly incomingScript?: string;
readonly history: ISqlDataSourceHistory;
@@ -65,6 +71,7 @@ export interface ISqlDataSource extends ILoadableState {
setName(name: string | null): void;
setProject(projectId: string | null): void;
setScript(script: string, source?: string): void;
+ setCursor(begin: number, end?: number): void;
setEditing(state: boolean): void;
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
setIncomingExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts
index 3d44e1bb22..830ce84b91 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts
@@ -8,7 +8,7 @@
import type { ISyncExecutor } from '@cloudbeaver/core-executor';
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
-import type { ISqlDataSource } from '../SqlDataSource/ISqlDataSource';
+import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource';
import type { SQLProposal } from '../SqlEditorService';
import type { ISQLScriptSegment, SQLParser } from '../SQLParser';
import type { ISQLEditorMode } from './SQLEditorModeContext';
@@ -18,13 +18,8 @@ export interface ISegmentExecutionData {
type: 'start' | 'end' | 'error';
}
-export interface ICursor {
- readonly begin: number;
- readonly end: number;
-}
-
export interface ISQLEditorData {
- readonly cursor: ICursor;
+ readonly cursor: ISqlEditorCursor;
activeSegmentMode: ISQLEditorMode;
readonly parser: SQLParser;
readonly dialect: SqlDialectInfo | undefined;
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
index 768084a133..23361e73ff 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
-import { action, autorun, computed, IReactionDisposer, observable, untracked } from 'mobx';
+import { action, autorun, computed, IReactionDisposer, observable, runInAction, untracked } from 'mobx';
import { useEffect } from 'react';
import { ConfirmationDialog, useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
@@ -19,7 +19,7 @@ import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@clou
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures';
-import type { ISqlDataSource } from '../SqlDataSource/ISqlDataSource';
+import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource';
import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService';
import { SqlDialectInfoService } from '../SqlDialectInfoService';
import { SqlEditorService } from '../SqlEditorService';
@@ -28,7 +28,7 @@ import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecu
import { OUTPUT_LOGS_TAB_ID } from '../SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID';
import { SqlQueryService } from '../SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
-import type { ICursor, ISQLEditorData } from './ISQLEditorData';
+import type { ISQLEditorData } from './ISQLEditorData';
import { SQLEditorModeContext } from './SQLEditorModeContext';
interface ISQLEditorDataPrivate extends ISQLEditorData {
@@ -44,7 +44,7 @@ interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly getLastAutocomplete: LastPromiseGetter;
readonly parseScript: LastPromiseGetter;
- cursor: ICursor;
+ cursor: ISqlEditorCursor;
readonlyState: boolean;
executingScript: boolean;
state: ISqlEditorTabState;
@@ -126,6 +126,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
return this.dataSource?.isIncomingChanges ?? false;
},
+ get cursor(): ISqlEditorCursor {
+ return this.dataSource?.cursor ?? { begin: 0, end: 0 };
+ },
+
get value(): string {
return this.dataSource?.script ?? '';
},
@@ -140,7 +144,6 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
onUpdate: new SyncExecutor(),
parser: new SQLParser(),
- cursor: { begin: 0, end: 0 },
readonlyState: false,
executingScript: false,
reactionDisposer: null,
@@ -177,17 +180,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
setCursor(begin: number, end = begin): void {
- if (begin > end) {
- throw new Error('Cursor begin can not be greater than the end of it');
- }
-
- const scriptLength = this.value.length;
-
- this.cursor = {
- begin: Math.min(begin, scriptLength),
- end: Math.min(end, scriptLength),
- };
- this.onUpdate.execute();
+ this.dataSource?.setCursor(begin, end);
},
getLastAutocomplete: createLastPromiseGetter(),
@@ -483,9 +476,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
isDisabled: computed,
value: computed,
readonly: computed,
+ cursor: computed,
activeSegmentMode: observable.ref,
hintsLimitIsMet: observable.ref,
- cursor: observable.ref,
readonlyState: observable,
executingScript: observable,
},
@@ -545,7 +538,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
const contexts = data.onMode.execute(data);
const activeSegmentMode = contexts.getContext(SQLEditorModeContext);
- action(() => {
+ runInAction(() => {
data.activeSegmentMode = activeSegmentMode;
});
});
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts
index 8c5ab81a63..5d0d8d31ac 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts
@@ -9,7 +9,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { uuid } from '@cloudbeaver/core-utils';
import { IDatabaseDataModel, IDatabaseResultSet, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
-import type { IResultGroup, ISqlEditorTabState, IStatisticsTab } from '../ISqlEditorTabState';
+import type { IResultGroup, IResultTab, ISqlEditorTabState, IStatisticsTab } from '../ISqlEditorTabState';
import type { IDataQueryOptions } from '../QueryDataSource';
@injectable()
@@ -199,14 +199,14 @@ export class SqlQueryResultService {
model: IDatabaseDataModel,
resultCount?: number,
) {
- this.updateResultTab(state, group, model, 0, resultCount);
+ this.createResultTabForGroup(state, group, model, 0, resultCount);
for (let i = 1; i < model.source.results.length; i++) {
- this.updateResultTab(state, group, model, i, resultCount);
+ this.createResultTabForGroup(state, group, model, i, resultCount);
}
}
- private updateResultTab(
+ private createResultTabForGroup(
state: ISqlEditorTabState,
group: IResultGroup,
model: IDatabaseDataModel,
@@ -226,6 +226,16 @@ export class SqlQueryResultService {
}
}
+ updateResultTab(state: ISqlEditorTabState, id: string, resultTab: Partial) {
+ const index = state.resultTabs.findIndex(tab => tab.tabId === id);
+
+ if (index === -1) {
+ return;
+ }
+
+ state.resultTabs[index] = { ...state.resultTabs[index], ...resultTab };
+ }
+
private createResultTab(state: ISqlEditorTabState, group: IResultGroup, indexInResultSet: number, results: number, resultCount?: number) {
const id = uuid();
@@ -233,6 +243,8 @@ export class SqlQueryResultService {
tabId: id,
groupId: group.groupId,
indexInResultSet,
+ presentationId: '',
+ valuePresentationId: null,
});
state.tabs.push({
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx
index e13e6dc344..1e13a72d34 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx
@@ -35,11 +35,9 @@ export const SqlResultPanel = observer(function SqlResultPanel({ state, i
const resultTab = state.resultTabs.find(tab => tab.tabId === id);
if (resultTab) {
- const group = state.resultGroups.find(group => group.groupId === resultTab.groupId)!;
-
return styled(style)(
-
+
,
);
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.m.css b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.m.css
new file mode 100644
index 0000000000..093b9fc4c6
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.m.css
@@ -0,0 +1,12 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2024 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+.tableViewerLoader {
+ padding: 8px;
+ padding-bottom: 0;
+}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.tsx
index e8b6c7a878..788c5d36fa 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultSetPanel.tsx
@@ -6,37 +6,48 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
-import { useState } from 'react';
-import styled, { css } from 'reshadow';
+import { useService } from '@cloudbeaver/core-di';
import { TableViewerLoader } from '@cloudbeaver/plugin-data-viewer';
-import type { IResultGroup, IResultTab } from '../ISqlEditorTabState';
-
-const styles = css`
- TableViewerLoader {
- padding: 8px;
- padding-bottom: 0;
- }
-`;
+import type { IResultTab, ISqlEditorTabState } from '../ISqlEditorTabState';
+import { SqlQueryResultService } from './SqlQueryResultService';
+import style from './SqlResultSetPanel.m.css';
interface Props {
- group: IResultGroup;
+ state: ISqlEditorTabState;
resultTab: IResultTab;
}
-export const SqlResultSetPanel = observer(function SqlResultSetPanel({ group, resultTab }) {
- const [presentationId, setPresentation] = useState('');
- const [valuePresentationId, setValuePresentation] = useState(null);
+export const SqlResultSetPanel = observer(function SqlResultSetPanel({ state, resultTab }) {
+ const sqlQueryResultService = useService(SqlQueryResultService);
+ const group = state.resultGroups.find(group => group.groupId === resultTab.groupId);
+
+ function onPresentationChange(presentationId: string) {
+ sqlQueryResultService.updateResultTab(state, resultTab.tabId, {
+ presentationId,
+ });
+ }
+
+ function onValuePresentationChange(valuePresentationId: string | null) {
+ sqlQueryResultService.updateResultTab(state, resultTab.tabId, {
+ valuePresentationId,
+ });
+ }
+
+ if (!group) {
+ throw new Error('Result group not found');
+ }
- return styled(styles)(
+ return (
,
+ presentationId={resultTab.presentationId}
+ valuePresentationId={resultTab.valuePresentationId}
+ onPresentationChange={onPresentationChange}
+ onValuePresentationChange={onValuePresentationChange}
+ />
);
});