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} + /> ); });