From 8a685ef47e69ce365d1665d772225ac37492f4dd Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:02:29 +0400 Subject: [PATCH] Cb 1036 server output log sql editor (#1999) * CB-2551. Fixed additional auth for disabled user * CB-3923. Added output reader logs * CB-3923. Added output reader logs * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3923. Refactor after review * CB-3924: add show output button * CB-3924: add output event handler and resource * CB-3924: add server output logs panel * CB-3924: fix bugs * CB-3923. Fixed checkstyle * CB-3924: fixes after review * CB-3923. Refactor after review * CB-3924: fixes after review * CB-3924: add line wrapper extension for editor * CB-3923. Refactor after review * CB-3924: show output logs for current context * CB-3924: fixes after review * CB-3924: add output logs icon * CB-3924: fixes after review * CB-3924: fixes after review * CB-3924: fixes after review * CB-3924: fix bug with not updated menus * CB-3924. Fix boolean type --------- Co-authored-by: Dmitry Osipov Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../schema/service.events.graphqls | 23 ++- .../schema/service.sql.graphqls | 3 +- .../server/jobs/SqlOutputLogReaderJob.java | 114 +++++++++++++++ .../service/sql/DBWServiceSQL.java | 4 +- .../service/sql/WebSQLProcessor.java | 23 ++- .../service/sql/WebServiceBindingSQL.java | 4 +- .../service/sql/impl/WebServiceSQL.java | 10 +- .../src/FormControls/InputField.m.css | 9 ++ .../src/FormControls/InputField.tsx | 5 +- .../src/queries/grid/asyncSqlExecuteQuery.gql | 2 + .../plugin-codemirror6/src/Editor.tsx | 44 +++++- .../src/useEditorDefaultExtensions.ts | 131 ++++++++++++------ .../DatabaseDataModel/IDatabaseDataOptions.ts | 1 + .../src/SqlEditorTabService.ts | 1 + .../public/icons/sql_output_logs.svg | 1 + .../public/icons/sql_output_logs_m.svg | 1 + .../public/icons/sql_output_logs_sm.svg | 1 + .../src/ISqlEditorTabState.ts | 6 + .../plugin-sql-editor/src/MenuBootstrap.ts | 5 + .../plugin-sql-editor/src/QueryDataSource.ts | 1 + .../src/SqlEditor/SqlEditorActionsMenu.tsx | 2 - .../src/SqlEditor/useSqlEditor.ts | 9 +- .../plugin-sql-editor/src/SqlEditorService.ts | 1 + .../plugin-sql-editor/src/SqlEditorView.ts | 2 + .../OutputLogs/ACTION_SHOW_OUTPUT_LOGS.ts | 7 + .../OutputLogs/IOutputLogTypes.ts | 2 + .../OutputLogs/OUTPUT_LOGS_FILTER_MENU.ts | 10 ++ .../OutputLogs/OUTPUT_LOGS_TAB_ID.ts | 1 + .../OutputLogs/OutputLogTypesFilterMenu.m.css | 14 ++ .../OutputLogs/OutputLogTypesFilterMenu.tsx | 39 ++++++ .../OutputLogs/OutputLogsEventHandler.ts | 23 +++ .../OutputLogs/OutputLogsPanel.tsx | 39 ++++++ .../OutputLogs/OutputLogsResource.ts | 64 +++++++++ .../OutputLogs/OutputLogsService.ts | 54 ++++++++ .../OutputLogs/OutputLogsToolbar.m.css | 6 + .../OutputLogs/OutputLogsToolbar.tsx | 45 ++++++ .../OutputLogs/OutputMenuBootstrap.ts | 128 +++++++++++++++++ .../OutputLogs/useOutputLogsPanelState.ts | 58 ++++++++ .../src/SqlResultTabs/SqlQueryService.ts | 6 + .../src/SqlResultTabs/SqlResultPanel.tsx | 9 ++ .../src/SqlResultTabs/SqlResultTabsService.ts | 10 +- .../actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.ts | 13 ++ .../KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT.ts | 14 ++ .../packages/plugin-sql-editor/src/index.ts | 4 + .../plugin-sql-editor/src/locales/en.ts | 3 + .../plugin-sql-editor/src/locales/it.ts | 3 + .../plugin-sql-editor/src/locales/ru.ts | 3 + .../plugin-sql-editor/src/locales/zh.ts | 3 + .../plugin-sql-editor/src/manifest.ts | 8 ++ 49 files changed, 908 insertions(+), 61 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java create mode 100644 webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg create mode 100644 webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_m.svg create mode 100644 webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_sm.svg create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/ACTION_SHOW_OUTPUT_LOGS.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/IOutputLogTypes.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_FILTER_MENU.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.m.css create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsEventHandler.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsPanel.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsResource.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsService.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.m.css create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputMenuBootstrap.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/useOutputLogsPanelState.ts create mode 100644 webapp/packages/plugin-sql-editor/src/actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.ts create mode 100644 webapp/packages/plugin-sql-editor/src/actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT.ts diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 1374063aea..5d21aaee78 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -28,7 +28,9 @@ enum CBServerEventId { cb_rm_project_removed, cb_object_permissions_updated, - cb_subject_permissions_updated + cb_subject_permissions_updated, + + cb_database_output_log_updated } # Events sent by client @@ -48,7 +50,8 @@ enum CBEventTopic { cb_scripts, cb_projects, cb_object_permissions, - cb_subject_permissions + cb_subject_permissions, + cb_database_output_log } # Base server event interface @@ -153,6 +156,22 @@ type CBProjectsActiveEvent implements CBClientEvent { projectIds: [String!]! # list of active projects } +# Database output log event +type CBDatabaseOutputLogEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + contextId: String! + messages: [WSOutputLogInfo!]! + eventTimestamp: Int! +} + +# Define the type for WSOutputLogInfo +type WSOutputLogInfo { + severity: String + message: String + # Add more fields as needed +} + extend type Query { emptyEvent: Boolean } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 0f27fd2a33..d008e2b557 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -289,7 +289,8 @@ extend type Mutation { sql: String!, resultId: ID, filter: SQLDataFilter, - dataFormat: ResultDataFormat # requested data format. May be ignored by server + dataFormat: ResultDataFormat, # requested data format. May be ignored by server + readLogs: Boolean # added 23.2.1 ): AsyncTaskInfo! # Read data from table diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java new file mode 100644 index 0000000000..b28329e3bf --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java @@ -0,0 +1,114 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jobs; + +import io.cloudbeaver.model.session.WebSession; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.exec.DBCStatement; +import org.jkiss.dbeaver.model.exec.output.DBCOutputSeverity; +import org.jkiss.dbeaver.model.exec.output.DBCOutputWriter; +import org.jkiss.dbeaver.model.exec.output.DBCServerOutputReader; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.websocket.event.WSOutputLogInfo; +import org.jkiss.dbeaver.model.websocket.event.session.WSOutputDBLogEvent; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import java.util.ArrayList; +import java.util.List; + + +public class SqlOutputLogReaderJob extends AbstractJob { + + private static final Log log = Log.getLog(SqlOutputLogReaderJob.class); + + @NotNull + private final WebSession webSession; + @NotNull + private final DBCExecutionContext dbcExecutionContext; + @NotNull + private final DBCStatement dbcStatement; + @NotNull + private final DBCServerOutputReader dbcServerOutputReader; + @NotNull + private final String contextInfoId; + + public SqlOutputLogReaderJob(@NotNull WebSession webSession, + @NotNull DBCExecutionContext dbcExecutionContext, + @NotNull DBCStatement dbcStatement, + @NotNull DBCServerOutputReader dbcServerOutputReader, + @NotNull String contextInfoId) { + super("Sql log state job"); + this.webSession = webSession; + this.dbcExecutionContext = dbcExecutionContext; + this.dbcStatement = dbcStatement; + this.dbcServerOutputReader = dbcServerOutputReader; + this.contextInfoId = contextInfoId; + } + + @Override + protected IStatus run(DBRProgressMonitor monitor) { + if (!DBWorkbench.getPlatform().isShuttingDown()) { + try { + if (!dbcStatement.isStatementClosed()) { + dumpOutput(monitor); + schedule(100); + } + } catch (Exception e) { + log.debug("Failed to execute job " + e.getMessage(), e); + } + } + return Status.OK_STATUS; + } + + private void dumpOutput(DBRProgressMonitor monitor) { + if (!monitor.isCanceled()) { + if (dbcServerOutputReader.isAsyncOutputReadSupported()) { + try { + if (!dbcStatement.isStatementClosed()) { + List messages = new ArrayList<>(); + dbcServerOutputReader.readServerOutput(monitor, dbcExecutionContext, null, dbcStatement, new DBCOutputWriter() { + @Override + public void println(@Nullable DBCOutputSeverity severity, @Nullable String message) { + if (message != null && severity != null) { + messages.add(new WSOutputLogInfo(severity.getName(), message)); + } + } + + @Override + public void flush() { + messages.clear(); + } + }); + webSession.addSessionEvent(new WSOutputDBLogEvent( + contextInfoId, + messages, + System.currentTimeMillis())); + } + } catch (DBCException e) { + log.error(e); + } + } + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 085eb95290..163076b5a5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -94,7 +94,9 @@ WebAsyncTaskInfo asyncExecuteQuery( @NotNull String sql, @Nullable String resultId, @Nullable WebSQLDataFilter filter, - @Nullable WebDataFormat dataFormat) throws DBException; + @Nullable WebDataFormat dataFormat, + boolean readLogs, + @NotNull WebSession webSession) throws DBException; @WebAction WebAsyncTaskInfo asyncReadDataFromContainer( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 04df960500..4ea954fae9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -20,6 +20,7 @@ import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; +import io.cloudbeaver.server.jobs.SqlOutputLogReaderJob; import org.eclipse.jface.text.Document; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -31,10 +32,12 @@ import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.edit.DBEPersistAction; import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.exec.output.DBCServerOutputReader; import org.jkiss.dbeaver.model.exec.plan.DBCPlan; import org.jkiss.dbeaver.model.exec.plan.DBCQueryPlanner; import org.jkiss.dbeaver.model.exec.plan.DBCQueryPlannerConfiguration; import org.jkiss.dbeaver.model.impl.AbstractExecutionSource; +import org.jkiss.dbeaver.model.impl.DefaultServerOutputReader; import org.jkiss.dbeaver.model.navigator.DBNDatabaseItem; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -152,7 +155,9 @@ public WebSQLExecuteInfo processQuery( @NotNull String sql, @Nullable String resultId, @Nullable WebSQLDataFilter filter, - @Nullable WebDataFormat dataFormat) throws DBWebException { + @Nullable WebDataFormat dataFormat, + @NotNull WebSession webSession, + boolean readLogs) throws DBWebException { if (filter == null) { // Use default filter filter = new WebSQLDataFilter(); @@ -203,6 +208,17 @@ public WebSQLExecuteInfo processQuery( webDataFilter.getOffset(), webDataFilter.getLimit())) { + SqlOutputLogReaderJob sqlOutputLogReaderJob = null; + if (readLogs) { + DBPDataSource dataSource = context.getDataSource(); + DBCServerOutputReader dbcServerOutputReader = DBUtils.getAdapter(DBCServerOutputReader.class, dataSource); + if (dbcServerOutputReader == null) { + dbcServerOutputReader = new DefaultServerOutputReader(); + } + sqlOutputLogReaderJob = new SqlOutputLogReaderJob( + webSession, context, dbStat, dbcServerOutputReader, contextInfo.getId()); + sqlOutputLogReaderJob.schedule(); + } // Set query timeout int queryTimeout = (int) session.getDataSource().getContainer().getPreferenceStore() .getDouble(WebSQLConstants.QUOTA_PROP_SQL_QUERY_TIMEOUT); @@ -220,6 +236,11 @@ public WebSQLExecuteInfo processQuery( } boolean hasResultSet = dbStat.executeStatement(); + + // Wait SqlLogStateJob, if its starts + if (sqlOutputLogReaderJob != null) { + sqlOutputLogReaderJob.join(); + } fillQueryResults(contextInfo, dataContainer, dbStat, hasResultSet, executeInfo, webDataFilter, dataFilter, dataFormat); } catch (DBException e) { throw new InvocationTargetException(e); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 1d125f05d5..fa8d1ed3cf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -160,7 +160,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("sql"), env.getArgument("resultId"), getDataFilter(env), - getDataFormat(env))) + getDataFormat(env), + CommonUtils.toBoolean(env.getArgument("readLogs")), + getWebSession(env))) .dataFetcher("asyncReadDataFromContainer", env -> getService(env).asyncReadDataFromContainer( getSQLContext(env), diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 0ac61588f8..2e1f5f6de0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -363,16 +363,18 @@ public WebAsyncTaskInfo asyncExecuteQuery( @NotNull String sql, @Nullable String resultId, @Nullable WebSQLDataFilter filter, - @Nullable WebDataFormat dataFormat) + @Nullable WebDataFormat dataFormat, + boolean readLogs, + @NotNull WebSession webSession) { - WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor() { + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { @Override - public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + public void run(DBRProgressMonitor monitor) throws InvocationTargetException { try { monitor.beginTask("Execute query", 1); monitor.subTask("Process query " + sql); WebSQLExecuteInfo executeResults = contextInfo.getProcessor().processQuery( - monitor, contextInfo, sql, resultId, filter, dataFormat); + monitor, contextInfo, sql, resultId, filter, dataFormat, webSession, readLogs); this.result = executeResults.getStatusMessage(); this.extendedResults = executeResults; } catch (Throwable e) { diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.m.css b/webapp/packages/core-blocks/src/FormControls/InputField.m.css index 8d4691d598..3531611b5b 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.m.css +++ b/webapp/packages/core-blocks/src/FormControls/InputField.m.css @@ -29,6 +29,15 @@ height: 100%; } } + +.customIconContainer { + composes: iconContainer; + right: 4px; + width: 24px; + height: 24px; + cursor: auto; +} + .input[disabled] + .iconContainer { cursor: auto; opacity: 0.8; diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx index 77dfb6ed06..1d6f2ea509 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { forwardRef, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'; +import React, { forwardRef, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'; import styled, { use } from 'reshadow'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; @@ -41,6 +41,7 @@ type BaseProps = Omit, 'onChange' | style?: ComponentStyle; canShowPassword?: boolean; onCustomCopy?: () => void; + icon?: React.ReactElement; }; type ControlledProps = BaseProps & { @@ -90,6 +91,7 @@ export const InputField: InputFieldType = observer( canShowPassword = true, onChange, onCustomCopy, + icon, ...rest }: ControlledProps | ObjectProps, ref, @@ -196,6 +198,7 @@ export const InputField: InputFieldType = observer( )} + {icon &&
{icon}
} {(description || passwordType) && (
diff --git a/webapp/packages/core-sdk/src/queries/grid/asyncSqlExecuteQuery.gql b/webapp/packages/core-sdk/src/queries/grid/asyncSqlExecuteQuery.gql index b2e1d0ec18..7aabbc989e 100644 --- a/webapp/packages/core-sdk/src/queries/grid/asyncSqlExecuteQuery.gql +++ b/webapp/packages/core-sdk/src/queries/grid/asyncSqlExecuteQuery.gql @@ -5,6 +5,7 @@ mutation asyncSqlExecuteQuery( $resultId: ID $filter: SQLDataFilter $dataFormat: ResultDataFormat + $readLogs: Boolean ) { taskInfo: asyncSqlExecuteQuery( connectionId: $connectionId @@ -13,6 +14,7 @@ mutation asyncSqlExecuteQuery( resultId: $resultId filter: $filter dataFormat: $dataFormat + readLogs: $readLogs ) { ...AsyncTaskInfo } diff --git a/webapp/packages/plugin-codemirror6/src/Editor.tsx b/webapp/packages/plugin-codemirror6/src/Editor.tsx index 1e6b7526c8..10be8ade09 100644 --- a/webapp/packages/plugin-codemirror6/src/Editor.tsx +++ b/webapp/packages/plugin-codemirror6/src/Editor.tsx @@ -19,9 +19,49 @@ import { useCodemirrorExtensions } from './useCodemirrorExtensions'; import { type IDefaultExtensions, useEditorDefaultExtensions } from './useEditorDefaultExtensions'; export const Editor = observer( - forwardRef(function Editor({ lineNumbers, extensions, ...rest }, ref) { + forwardRef(function Editor( + { + extensions, + lineNumbers, + tooltips, + highlightSpecialChars, + syntaxHighlighting, + bracketMatching, + dropCursor, + crosshairCursor, + foldGutter, + highlightActiveLineGutter, + highlightSelectionMatches, + highlightActiveLine, + indentOnInput, + rectangularSelection, + keymap, + lineWrapping, + ...rest + }, + ref, + ) { extensions = useCodemirrorExtensions(extensions); - const defaultExtensions = useEditorDefaultExtensions({ lineNumbers }); + + + const defaultExtensions = useEditorDefaultExtensions({ + lineNumbers, + tooltips, + highlightSpecialChars, + syntaxHighlighting, + bracketMatching, + dropCursor, + crosshairCursor, + foldGutter, + highlightActiveLineGutter, + highlightSelectionMatches, + highlightActiveLine, + indentOnInput, + rectangularSelection, + keymap, + lineWrapping, + }); + extensions.set(...defaultExtensions); return styled(EDITOR_BASE_STYLES)( diff --git a/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts b/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts index cbfd1ca01b..8830f64967 100644 --- a/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts +++ b/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts @@ -12,6 +12,7 @@ import { Compartment, Extension } from '@codemirror/state'; import { crosshairCursor, dropCursor, + EditorView, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, @@ -21,9 +22,9 @@ import { tooltips, } from '@codemirror/view'; import { classHighlighter } from '@lezer/highlight'; -import { useMemo } from 'react'; +import { useRef } from 'react'; -import { clsx, GlobalConstants } from '@cloudbeaver/core-utils'; +import { clsx, GlobalConstants, isObjectsEqual } from '@cloudbeaver/core-utils'; // @TODO allow to configure bindings outside of the component const DEFAULT_KEY_MAP = defaultKeymap.filter(binding => binding.mac !== 'Ctrl-f' && binding.key !== 'Mod-Enter'); @@ -34,55 +35,101 @@ DEFAULT_KEY_MAP.push({ run: () => true, }); +const defaultExtensionsFlags: IDefaultExtensions = { + lineNumbers: false, + tooltips: true, + highlightSpecialChars: true, + syntaxHighlighting: true, + bracketMatching: true, + dropCursor: true, + crosshairCursor: true, + foldGutter: true, + highlightActiveLineGutter: true, + highlightSelectionMatches: true, + highlightActiveLine: true, + indentOnInput: true, + rectangularSelection: true, + keymap: true, + lineWrapping: false, +}; + export interface IDefaultExtensions { lineNumbers?: boolean; + tooltips?: boolean; + highlightSpecialChars?: boolean; + syntaxHighlighting?: boolean; + bracketMatching?: boolean; + dropCursor?: boolean; + crosshairCursor?: boolean; + foldGutter?: boolean; + highlightActiveLineGutter?: boolean; + highlightSelectionMatches?: boolean; + highlightActiveLine?: boolean; + indentOnInput?: boolean; + rectangularSelection?: boolean; + keymap?: boolean; + lineWrapping?: boolean; } +const extensionMap = { + lineNumbers, + tooltips: () => tooltips({ parent: document.body }), + highlightSpecialChars, + syntaxHighlighting: () => syntaxHighlighting(classHighlighter), + bracketMatching, + dropCursor, + highlightSelectionMatches, + crosshairCursor, + foldGutter: () => + foldGutter({ + markerDOM: (open: boolean) => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttributeNS(null, 'viewBox', '0 0 15 8'); + svg.style.maxWidth = '100%'; + svg.style.maxHeight = '100%'; + + const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', GlobalConstants.absoluteUrl('/icons/icons.svg#angle')); + svg.appendChild(use); + + const element = document.createElement('div'); + element.appendChild(svg); + element.className = clsx('cm-gutterElement-icon', open ? 'cm-foldGutter-open' : 'cm-foldGutter-folded'); + + return element; + }, + }), + highlightActiveLineGutter, + highlightActiveLine, + indentOnInput, + rectangularSelection, + keymap: () => keymap.of(DEFAULT_KEY_MAP), + lineWrapping: () => EditorView.lineWrapping, +}; + const DEFAULT_EXTENSIONS_COMPARTMENT = new Compartment(); /** Provides the necessary extensions to establish a basic editor */ export function useEditorDefaultExtensions(options?: IDefaultExtensions): [Compartment, Extension] { - return useMemo(() => { - const extensions = []; - if (options?.lineNumbers) { - extensions.push(lineNumbers()); - } + const previousOptions = useRef(options); + const isOptionsChanged = !isObjectsEqual(options, previousOptions.current); + const extensions = useRef<[Compartment, Extension] | null>(null); - extensions.push( - tooltips({ - parent: document.body, - }), - highlightSpecialChars(), - highlightSelectionMatches(), - syntaxHighlighting(classHighlighter), - bracketMatching(), - dropCursor(), - crosshairCursor(), - foldGutter({ - markerDOM: (open: boolean) => { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttributeNS(null, 'viewBox', '0 0 15 8'); - svg.style.maxWidth = '100%'; - svg.style.maxHeight = '100%'; + if (isOptionsChanged || extensions.current === null) { + previousOptions.current = options; + extensions.current = createExtensions(options); + } - const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', GlobalConstants.absoluteUrl('/icons/icons.svg#angle')); - svg.appendChild(use); - - const element = document.createElement('div'); - element.appendChild(svg); - element.className = clsx('cm-gutterElement-icon', open ? 'cm-foldGutter-open' : 'cm-foldGutter-folded'); - - return element; - }, - }), - highlightActiveLineGutter(), - highlightActiveLine(), - indentOnInput(), - rectangularSelection(), - keymap.of(DEFAULT_KEY_MAP), - ); + return extensions.current; +} - return [DEFAULT_EXTENSIONS_COMPARTMENT, extensions]; - }, [options?.lineNumbers]); +function createExtensions(options?: IDefaultExtensions): [Compartment, Extension] { + const extensions = Object.entries(defaultExtensionsFlags) + .filter(([key, isEnabled]) => options?.[key as keyof typeof options] ?? isEnabled) + .map(([key]) => { + const extensionFunction = extensionMap[key as keyof typeof extensionMap]; + return extensionFunction?.(); + }) + .filter(Boolean); + return [DEFAULT_EXTENSIONS_COMPARTMENT, extensions]; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts index 2b6c946d3a..50f56e43e0 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts @@ -14,4 +14,5 @@ export interface IDatabaseDataOptions { catalog?: string; whereFilter: string; constraints: SqlDataFilterConstraint[]; + readLogs?: boolean; } 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 400126f0e3..f1fd0fe203 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -309,6 +309,7 @@ export class SqlEditorTabService extends Bootstrap { tab.handlerState.resultTabs = observable([]); tab.handlerState.executionPlanTabs = observable([]); tab.handlerState.statisticsTabs = observable([]); + tab.handlerState.outputLogsTab = undefined; return true; } diff --git a/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg new file mode 100644 index 0000000000..bc9e4420c2 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_m.svg b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_m.svg new file mode 100644 index 0000000000..d4d1f74ad4 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_m.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_sm.svg b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_sm.svg new file mode 100644 index 0000000000..bda167f39f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs_sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index 7c2c59154c..a117d81396 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -5,6 +5,7 @@ * 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'; export interface IResultTab { tabId: string; @@ -41,6 +42,10 @@ export interface IExecutionPlanTab { options?: Record; } +export interface IOutputLogsTab extends ISqlEditorResultTab { + selectedLogTypes: IOutputLogType[]; +} + export interface ISqlEditorTabState { editorId: string; datasourceKey: string; @@ -54,6 +59,7 @@ export interface ISqlEditorTabState { resultTabs: IResultTab[]; statisticsTabs: IStatisticsTab[]; executionPlanTabs: IExecutionPlanTab[]; + outputLogsTab?: IOutputLogsTab; // mode currentModeId?: string; diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 2652018a18..a99f4a8511 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -22,11 +22,13 @@ import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECU import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT'; import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT'; import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN'; +import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; import { KEY_BINDING_SQL_EDITOR_EXECUTE } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE'; import { KEY_BINDING_SQL_EDITOR_EXECUTE_NEW } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_NEW'; import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT'; import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT'; import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN'; +import { KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT'; import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA'; @@ -57,11 +59,13 @@ export class MenuBootstrap extends Bootstrap { ACTION_SQL_EDITOR_EXECUTE_NEW, ACTION_SQL_EDITOR_EXECUTE_SCRIPT, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ACTION_SQL_EDITOR_SHOW_OUTPUT, ].includes(action) ) { return false; } + // TODO we have to add check for output action ? if ( !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && [ACTION_SQL_EDITOR_EXECUTE, ACTION_SQL_EDITOR_EXECUTE_NEW, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN].includes(action) @@ -77,6 +81,7 @@ export class MenuBootstrap extends Bootstrap { ACTION_REDO, ACTION_UNDO, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ACTION_SQL_EDITOR_SHOW_OUTPUT, ].includes(action); }, isDisabled: (context, action) => !context.has(DATA_CONTEXT_SQL_EDITOR_DATA), diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 461544edba..95355bff36 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -199,6 +199,7 @@ export class QueryDataSource(function SqlEditorActionsMenu({ state, context, className }) { const styles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles); const menu = useMenu({ menu: SQL_EDITOR_ACTIONS_MENU, context }); - menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state); return ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index a313d6475a..7234ee2131 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -25,6 +25,8 @@ import { SqlDialectInfoService } from '../SqlDialectInfoService'; import { SqlEditorService } from '../SqlEditorService'; import { ISQLScriptSegment, SQLParser } from '../SQLParser'; import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService'; +import { OUTPUT_LOGS_TAB_ID } from '../SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID'; +import { OutputLogsService } from '../SqlResultTabs/OutputLogs/OutputLogsService'; import { SqlQueryService } from '../SqlResultTabs/SqlQueryService'; import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService'; import type { ICursor, ISQLEditorData } from './ISQLEditorData'; @@ -68,6 +70,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const sqlResultTabsService = useService(SqlResultTabsService); const commonDialogService = useService(CommonDialogService); const sqlDataSourceService = useService(SqlDataSourceService); + const sqlOutputLogsService = useService(OutputLogsService); const data = useObservableRef( () => ({ @@ -310,9 +313,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } if (this.state.tabs.length) { + const processableTabs = this.state.tabs.filter(tab => tab.id !== OUTPUT_LOGS_TAB_ID); + const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'sql_editor_close_result_tabs_dialog_title', - message: `Do you want to close ${this.state.tabs.length} tabs before executing script?`, + message: `Do you want to close ${processableTabs.length} tabs before executing script?`, confirmActionText: 'ui_yes', extraStatus: 'no', }); @@ -324,7 +329,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return; } - this.sqlResultTabsService.removeResultTabs(this.state); + this.sqlResultTabsService.removeResultTabs(this.state, [OUTPUT_LOGS_TAB_ID]); } else if (result === DialogueStateResult.Rejected) { return; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts index 07d65961e8..7d625aaa36 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -72,6 +72,7 @@ export class SqlEditorService { resultTabs: observable([]), executionPlanTabs: observable([]), statisticsTabs: observable([]), + outputLogsTab: undefined, currentModeId: undefined, modeState: observable([]), }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts index 58f8bb152e..0178d42e5c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts @@ -14,6 +14,7 @@ import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECU import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT'; import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT'; import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN'; +import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; @injectable() export class SqlEditorView extends View { @@ -27,6 +28,7 @@ export class SqlEditorView extends View { ACTION_UNDO, ACTION_REDO, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ACTION_SQL_EDITOR_SHOW_OUTPUT, ACTION_SAVE, ); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/ACTION_SHOW_OUTPUT_LOGS.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/ACTION_SHOW_OUTPUT_LOGS.ts new file mode 100644 index 0000000000..ffe85a38f9 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/ACTION_SHOW_OUTPUT_LOGS.ts @@ -0,0 +1,7 @@ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_SHOW_OUTPUT_LOGS = createAction('action-show_output_logs', { + label: 'sql_editor_output_logs_button_tooltip', + icon: '/icons/sql_output_logs.svg', + tooltip: 'sql_editor_output_logs_button_tooltip', +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/IOutputLogTypes.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/IOutputLogTypes.ts new file mode 100644 index 0000000000..5078ee9ff3 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/IOutputLogTypes.ts @@ -0,0 +1,2 @@ +export const OUTPUT_LOG_TYPES = ['Debug', 'Log', 'Info', 'Notice', 'Warning', 'Error'] as const; +export type IOutputLogType = (typeof OUTPUT_LOG_TYPES)[number]; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_FILTER_MENU.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_FILTER_MENU.ts new file mode 100644 index 0000000000..e1f53931bd --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_FILTER_MENU.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createMenu } from '@cloudbeaver/core-view'; + +export const OUTPUT_LOGS_FILTER_MENU = createMenu('output_logs_filter_menu', 'Output Logs'); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.ts new file mode 100644 index 0000000000..b88237948f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.ts @@ -0,0 +1 @@ +export const OUTPUT_LOGS_TAB_ID = 'output_logs'; diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.m.css b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.m.css new file mode 100644 index 0000000000..83bd17790b --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.m.css @@ -0,0 +1,14 @@ +.contextMenu { + padding: 0; + height: 24px; + width: 24px; + display: flex; + box-sizing: border-box; + align-items: center; + justify-content: center; + + & .icon { + width: 16px; + height: 100%; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.tsx new file mode 100644 index 0000000000..9b31a9b4c8 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogTypesFilterMenu.tsx @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import React, { useEffect } from 'react'; + +import { Icon, s, useS } from '@cloudbeaver/core-blocks'; +import { ContextMenu } from '@cloudbeaver/core-ui'; +import { useMenu } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../../DATA_CONTEXT_SQL_EDITOR_STATE'; +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import { OUTPUT_LOGS_FILTER_MENU } from './OUTPUT_LOGS_FILTER_MENU'; +import style from './OutputLogTypesFilterMenu.m.css'; + +interface Props { + sqlEditorTabState: ISqlEditorTabState; +} + +export const OutputLogsFilterMenu = observer(function OutputLogTypesFilterMenu({ sqlEditorTabState }) { + const styles = useS(style); + const menu = useMenu({ + menu: OUTPUT_LOGS_FILTER_MENU, + }); + + useEffect(() => { + menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, sqlEditorTabState); + }, []); + + return ( + + + + ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsEventHandler.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsEventHandler.ts new file mode 100644 index 0000000000..e800c4a631 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsEventHandler.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { ISessionEvent, SessionEventSource, SessionEventTopic, TopicEventHandler } from '@cloudbeaver/core-root'; +import type { CbDatabaseOutputLogEvent, CbSessionLogEvent as ISessionLogEvent } from '@cloudbeaver/core-sdk'; + +export { type ISessionLogEvent }; + +@injectable() +export class OutputLogsEventHandler extends TopicEventHandler { + constructor(sessionEventSource: SessionEventSource) { + super(SessionEventTopic.CbDatabaseOutputLog, sessionEventSource); + } + + map(event: CbDatabaseOutputLogEvent) { + return event; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsPanel.tsx new file mode 100644 index 0000000000..246fad90eb --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsPanel.tsx @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, Group, s, useResource, useS } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { EditorLoader } from '@cloudbeaver/plugin-codemirror6'; + +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import { OutputLogsResource } from './OutputLogsResource'; +import { OutputLogsService } from './OutputLogsService'; +import { OutputLogsToolbar } from './OutputLogsToolbar'; +import { useOutputLogsPanelState } from './useOutputLogsPanelState'; + +interface Props { + sqlEditorTabState: ISqlEditorTabState; +} + +export const OutputLogsPanel = observer(function SqlOutputLogsPanel({ sqlEditorTabState }) { + const outputLogsService = useService(OutputLogsService); + const { data } = useResource(SqlOutputLogsPanel, OutputLogsResource, undefined); + const outputLogs = outputLogsService.getOutputLogs(data, sqlEditorTabState); + + const state = useOutputLogsPanelState(outputLogs, sqlEditorTabState); + + return ( + + + + {data && } + + + ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsResource.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsResource.ts new file mode 100644 index 0000000000..4a03614823 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsResource.ts @@ -0,0 +1,64 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ConnectionExecutionContextResource } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { ServerEventId } from '@cloudbeaver/core-root'; +import { CachedDataResource, CbDatabaseOutputLogEvent } from '@cloudbeaver/core-sdk'; + +import type { IOutputLogType } from './IOutputLogTypes'; +import { OutputLogsEventHandler } from './OutputLogsEventHandler'; + +export interface IOutputLog { + message: string; + severity: IOutputLogType; + contextId: string; + timestamp: number; +} + +@injectable() +export class OutputLogsResource extends CachedDataResource { + constructor( + sqlOutputLogsEventHandler: OutputLogsEventHandler, + private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, + ) { + super(() => []); + + sqlOutputLogsEventHandler.onEvent( + ServerEventId.CbDatabaseOutputLogUpdated, + (event: CbDatabaseOutputLogEvent) => { + this.collectMessagesFromEvent(event); + }, + undefined, + this, + ); + + // hack, we need to call this.use() to initialize resource at startup + this.use(undefined); + + this.connectionExecutionContextResource.onItemDelete.addHandler(key => { + this.setData(this.data.filter(log => log.contextId !== key)); + }); + } + + private collectMessagesFromEvent(event: CbDatabaseOutputLogEvent) { + const newLogs = event.messages.map(message => ({ + message: message.message, + severity: message.severity, + contextId: event.contextId, + timestamp: event.eventTimestamp, + })) as IOutputLog[]; + + const updatedData: IOutputLog[] = (this.data || []).concat(newLogs); + + this.setData(updatedData); + } + + protected async loader(): Promise { + return this.data; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsService.ts new file mode 100644 index 0000000000..f518517bf7 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsService.ts @@ -0,0 +1,54 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; + +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import { SqlDataSourceService } from '../../SqlDataSource/SqlDataSourceService'; +import { OUTPUT_LOG_TYPES } from './IOutputLogTypes'; +import { OUTPUT_LOGS_TAB_ID } from './OUTPUT_LOGS_TAB_ID'; +import type { IOutputLog } from './OutputLogsResource'; + +@injectable() +export class OutputLogsService { + constructor(private readonly sqlDataSourceService: SqlDataSourceService) {} + + async showOutputLogs(editorState: ISqlEditorTabState): Promise { + this.createOutputLogsTab(editorState); + editorState.currentTabId = OUTPUT_LOGS_TAB_ID; + } + + removeOutputLogsTab(state: ISqlEditorTabState, tabId: string): void { + if (tabId === OUTPUT_LOGS_TAB_ID) { + state.outputLogsTab = undefined; + } + } + + private createOutputLogsTab(state: ISqlEditorTabState) { + const order = Math.max(0, ...state.tabs.map(tab => tab.order + 1)); + + if (state.tabs.find(tab => tab.id === OUTPUT_LOGS_TAB_ID)) { + return; + } + + const tab = { + id: OUTPUT_LOGS_TAB_ID, + name: 'Output', + icon: '/icons/sql_output_logs.svg', + order, + }; + + state.outputLogsTab = { ...tab, selectedLogTypes: [...OUTPUT_LOG_TYPES] }; + state.tabs.push({ ...tab }); + } + + getOutputLogs(events: IOutputLog[], editorState: ISqlEditorTabState) { + const dataSource = this.sqlDataSourceService.get(editorState.editorId); + + return events.filter(event => event.contextId === dataSource?.executionContext?.id); + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.m.css b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.m.css new file mode 100644 index 0000000000..e5faba5c7a --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.m.css @@ -0,0 +1,6 @@ +.searchIcon { + width: 24px; + height: 24px; + display: flex; + border-radius: var(--theme-menu-bar-small-action-radius); +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.tsx new file mode 100644 index 0000000000..8f6002dd87 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsToolbar.tsx @@ -0,0 +1,45 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import React from 'react'; + +import { Container, Icon, InputField, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import style from './OutputLogsToolbar.m.css'; +import { OutputLogsFilterMenu } from './OutputLogTypesFilterMenu'; +import type { SqlOutputLogsPanelState } from './useOutputLogsPanelState'; + +interface Props { + state: SqlOutputLogsPanelState; + sqlEditorTabState: ISqlEditorTabState; +} + +export const OutputLogsToolbar = observer(function SqlOutputLogsToolbar({ state, sqlEditorTabState }) { + const styles = useS(style); + const translate = useTranslate(); + + return ( + + + +
+ } + fill + onChange={value => state.setSearchValue(value.toString())} + /> + + + + + ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputMenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputMenuBootstrap.ts new file mode 100644 index 0000000000..c23292cd31 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputMenuBootstrap.ts @@ -0,0 +1,128 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { ActionService, KeyBindingService, MenuCheckboxItem, MenuService } from '@cloudbeaver/core-view'; + +import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from '../../actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; +import { KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT } from '../../actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../../DATA_CONTEXT_SQL_EDITOR_STATE'; +import { ESqlDataSourceFeatures } from '../../SqlDataSource/ESqlDataSourceFeatures'; +import { SqlDataSourceService } from '../../SqlDataSource/SqlDataSourceService'; +import { SQL_EDITOR_ACTIONS_MENU } from '../../SqlEditor/SQL_EDITOR_ACTIONS_MENU'; +import { ACTION_SHOW_OUTPUT_LOGS } from './ACTION_SHOW_OUTPUT_LOGS'; +import { OUTPUT_LOG_TYPES } from './IOutputLogTypes'; +import { OUTPUT_LOGS_FILTER_MENU } from './OUTPUT_LOGS_FILTER_MENU'; +import { OutputLogsService } from './OutputLogsService'; + +@injectable() +export class OutputMenuBootstrap extends Bootstrap { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + private readonly outputLogsService: OutputLogsService, + private readonly sqlDataSourceService: SqlDataSourceService, + private readonly keyBindingService: KeyBindingService, + ) { + super(); + } + + register(): void | Promise { + this.menuService.addCreator({ + menus: [OUTPUT_LOGS_FILTER_MENU], + getItems: context => { + const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (!state) { + return []; + } + + const outputLogsTabState = state?.outputLogsTab; + const items = []; + + for (const logType of OUTPUT_LOG_TYPES) { + items.push( + new MenuCheckboxItem( + { + id: logType, + label: logType, + tooltip: logType, + }, + { + onSelect: () => { + if (outputLogsTabState?.selectedLogTypes) { + if (outputLogsTabState.selectedLogTypes.includes(logType)) { + outputLogsTabState.selectedLogTypes = outputLogsTabState.selectedLogTypes.filter(type => type !== logType); + return; + } + outputLogsTabState.selectedLogTypes = [...outputLogsTabState.selectedLogTypes, logType]; + } + }, + }, + { + isChecked: () => !!outputLogsTabState?.selectedLogTypes.includes(logType), + }, + ), + ); + } + + return items; + }, + }); + + this.registerOutputLogsAction(); + } + + private registerOutputLogsAction() { + this.actionService.addHandler({ + id: 'output-logs-handler', + isActionApplicable: (context, action): boolean => { + const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (state && action === ACTION_SHOW_OUTPUT_LOGS) { + const sqlDataSource = this.sqlDataSourceService.get(state.editorId); + const isQuery = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isExecutable = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.executable); + + if (isQuery && isExecutable) { + return true; + } + } + + return false; + }, + + handler: async (context, action) => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (action === ACTION_SHOW_OUTPUT_LOGS) { + this.outputLogsService.showOutputLogs(state); + } + }, + }); + + this.menuService.addCreator({ + menus: [SQL_EDITOR_ACTIONS_MENU], + getItems: (context, items) => [...items, ACTION_SHOW_OUTPUT_LOGS], + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'sql-editor-show-output', + binding: KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT, + isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_SHOW_OUTPUT, + handler: (context, action) => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (action === ACTION_SQL_EDITOR_SHOW_OUTPUT) { + this.outputLogsService.showOutputLogs(state); + } + }, + }); + } + + async load(): Promise {} +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/useOutputLogsPanelState.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/useOutputLogsPanelState.ts new file mode 100644 index 0000000000..f2cfa2e591 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/useOutputLogsPanelState.ts @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import type { WsOutputLogInfo } from '@cloudbeaver/core-sdk'; + +import type { ISqlEditorTabState } from '../../ISqlEditorTabState'; +import type { IOutputLog } from './OutputLogsResource'; + +export interface SqlOutputLogsPanelState { + searchValue: string; + setSearchValue: (value: string) => void; + logMessages: WsOutputLogInfo['message'][]; + readonly resultValue: string; + readonly filteredLogs: WsOutputLogInfo[]; +} +export const useOutputLogsPanelState = (outputLogs: IOutputLog[], sqlEditorTabState: ISqlEditorTabState) => + useObservableRef( + () => ({ + searchValue: '', + setSearchValue(value: string) { + this.searchValue = value; + }, + get filteredLogs() { + const selectedLogTypes = sqlEditorTabState.outputLogsTab?.selectedLogTypes; + + if (!selectedLogTypes?.length) { + return []; + } + + return outputLogs.filter(log => { + if (!selectedLogTypes.includes(log.severity)) { + return false; + } + if (this.searchValue.length > 0 && !log.message?.includes(this.searchValue)) { + return false; + } + return true; + }); + }, + get resultValue() { + return this.filteredLogs.map(log => `[${log.severity}] ${log.message}`).join('\n'); + }, + }), + { + searchValue: observable.ref, + setSearchValue: action.bound, + filteredLogs: computed, + resultValue: computed, + }, + false, + ); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts index 58ca739f4e..5599760742 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts @@ -144,6 +144,8 @@ export class SqlQueryService { } const editable = this.dataViewerService.isDataEditable(connectionInfo); + const isOutputLogsTabOpened = !!editorState.outputLogsTab; + model .setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly) .setOptions({ @@ -151,6 +153,7 @@ export class SqlQueryService { connectionKey, constraints: [], whereFilter: '', + readLogs: isOutputLogsTabOpened, }) .source.setExecutionContext(executionContext) .setSupportedDataFormats(connectionInfo.supportedDataFormats); @@ -218,6 +221,8 @@ export class SqlQueryService { statistics.modelId = model.id; const editable = this.dataViewerService.isDataEditable(connectionInfo); + const isOutputLogsTabOpened = !!editorState.outputLogsTab; + model .setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly) .setOptions({ @@ -225,6 +230,7 @@ export class SqlQueryService { connectionKey, constraints: [], whereFilter: '', + readLogs: isOutputLogsTabOpened, }) .source.setExecutionContext(executionContext) .setSupportedDataFormats(connectionInfo.supportedDataFormats); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx index 523fa6876d..89b6caeb03 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel.tsx @@ -10,6 +10,7 @@ import styled, { css } from 'reshadow'; import type { ISqlEditorTabState } from '../ISqlEditorTabState'; import { SqlExecutionPlanPanel } from './ExecutionPlan/SqlExecutionPlanPanel'; +import { OutputLogsPanel } from './OutputLogs/OutputLogsPanel'; import { SqlResultSetPanel } from './SqlResultSetPanel'; import { SqlScriptStatisticsPanel } from './SqlScriptStatisticsPanel'; @@ -59,5 +60,13 @@ export const SqlResultPanel = observer(function SqlResultPanel({ state, i ); } + if (state.outputLogsTab) { + return styled(style)( + + + , + ); + } + return null; }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 2c2a214b20..138c7f9806 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -11,6 +11,7 @@ import { injectable } from '@cloudbeaver/core-di'; import type { ISqlEditorResultTab, ISqlEditorTabState } from '../ISqlEditorTabState'; import { SqlExecutionPlanService } from './ExecutionPlan/SqlExecutionPlanService'; +import { OutputLogsService } from './OutputLogs/OutputLogsService'; import { SqlQueryResultService } from './SqlQueryResultService'; import { SqlQueryService } from './SqlQueryService'; @@ -20,6 +21,7 @@ export class SqlResultTabsService { private readonly sqlQueryService: SqlQueryService, private readonly sqlQueryResultService: SqlQueryResultService, private readonly sqlExecutionPlanService: SqlExecutionPlanService, + private readonly sqlOutputLogsService: OutputLogsService, ) { makeObservable(this, { removeResultTabs: action, @@ -62,8 +64,11 @@ export class SqlResultTabsService { return true; } - removeResultTabs(state: ISqlEditorTabState): void { - for (const tab of state.tabs.slice()) { + removeResultTabs(state: ISqlEditorTabState, excludedTabIds?: string[]): void { + for (const tab of state.tabs) { + if (excludedTabIds?.includes(tab.id)) { + continue; + } this.removeTab(state, tab); } } @@ -74,6 +79,7 @@ export class SqlResultTabsService { this.sqlQueryService.removeStatisticsTab(state, tab.id); this.sqlQueryResultService.removeResultTab(state, tab.id); this.sqlExecutionPlanService.removeExecutionPlanTab(state, tab.id); + this.sqlOutputLogsService.removeOutputLogsTab(state, tab.id); if (state.currentTabId === tab.id) { if (state.tabs.length > 0) { diff --git a/webapp/packages/plugin-sql-editor/src/actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.ts b/webapp/packages/plugin-sql-editor/src/actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.ts new file mode 100644 index 0000000000..ef3a9deee5 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_SQL_EDITOR_SHOW_OUTPUT = createAction('sql-editor-show-output', { + icon: '/icons/sql_output.svg', // todo change icon + label: 'sql_editor_output_logs_button_tooltip', +}); diff --git a/webapp/packages/plugin-sql-editor/src/actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT.ts b/webapp/packages/plugin-sql-editor/src/actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT.ts new file mode 100644 index 0000000000..2508b02b88 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createKeyBinding } from '@cloudbeaver/core-view'; + +export const KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT = createKeyBinding({ + id: 'sql-editor-show-output', + keys: 'shift+ctrl+o', + preventDefault: true, +}); diff --git a/webapp/packages/plugin-sql-editor/src/index.ts b/webapp/packages/plugin-sql-editor/src/index.ts index 5ee2f5bd1d..755f70e50a 100644 --- a/webapp/packages/plugin-sql-editor/src/index.ts +++ b/webapp/packages/plugin-sql-editor/src/index.ts @@ -10,6 +10,7 @@ export * from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT'; export * from './actions/ACTION_SQL_EDITOR_EXECUTE'; export * from './actions/ACTION_SQL_EDITOR_FORMAT'; export * from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN'; +export * from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; export * from './SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState'; export * from './SqlDataSource/LocalStorage/LocalStorageSqlDataSource'; export * from './SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap'; @@ -27,6 +28,9 @@ export * from './SqlEditor/SQL_EDITOR_TOOLS_MENU'; export * from './SqlEditor/SQLEditorModeContext'; export * from './SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID'; export * from './SqlResultTabs/SqlResultTabsService'; +export * from './SqlResultTabs/OutputLogs/OutputLogsEventHandler'; +export * from './SqlResultTabs/OutputLogs/OutputLogsResource'; +export * from './SqlResultTabs/OutputLogs/OutputLogsService'; export * from './DATA_CONTEXT_SQL_EDITOR_STATE'; export * from './getSqlEditorName'; export * from './QueryDataSource'; diff --git a/webapp/packages/plugin-sql-editor/src/locales/en.ts b/webapp/packages/plugin-sql-editor/src/locales/en.ts index 3c20f7b3bd..54308fd446 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/en.ts @@ -7,6 +7,9 @@ export default [ ['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'], ['sql_editor_hint_empty', 'There is no proposals...'], ['sql_editor_execution_plan_button_tooltip', 'Explain execution plan (Shift + Ctrl + E)'], + ['sql_editor_output_logs_button_tooltip', 'Show server output (Shift + Ctrl + O)'], + ['sql_editor_output_logs_tab_title', 'Output'], + ['sql_editor_output_logs_input_placeholder', 'Enter a part of a message to search for here'], ['sql_editor_sql_execution_button_tooltip', 'Execute SQL Statement (Ctrl + Enter)'], ['sql_editor_sql_execution_new_tab_button_tooltip', 'Execute SQL Statement in new tab (Ctrl + \\)(Shift + Ctrl + Enter)'], ['sql_editor_sql_execution_script_button_tooltip', 'Execute SQL Script (Alt + X)'], diff --git a/webapp/packages/plugin-sql-editor/src/locales/it.ts b/webapp/packages/plugin-sql-editor/src/locales/it.ts index e0649ef0a6..8071d17e7e 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/it.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/it.ts @@ -7,6 +7,9 @@ export default [ ['sql_editor_placeholder', 'Esegui la query con Ctrl+Enter per vedere i risultati'], ['sql_editor_hint_empty', 'There is no proposals...'], ['sql_editor_execution_plan_button_tooltip', 'Mostra il piano di esecuzione (Shift + Ctrl + E)'], + ['sql_editor_output_logs_button_tooltip', 'Show server output (Shift + Ctrl + O)'], + ['sql_editor_output_logs_tab_title', 'Output'], + ['sql_editor_output_logs_input_placeholder', 'Enter a part of a message to search for here'], ['sql_editor_sql_execution_button_tooltip', "Esegui l'istruzione SQL (Ctrl + Enter)"], ['sql_editor_sql_execution_new_tab_button_tooltip', "Esegui l'istruzione SQL in una nuova tab (Ctrl + \\)(Shift + Ctrl + Enter)"], ['sql_editor_sql_execution_script_button_tooltip', 'Esegui lo script SQL (Alt + X)'], diff --git a/webapp/packages/plugin-sql-editor/src/locales/ru.ts b/webapp/packages/plugin-sql-editor/src/locales/ru.ts index f16baf789f..f639337962 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/ru.ts @@ -7,6 +7,9 @@ export default [ ['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'], ['sql_editor_hint_empty', 'Нет авто-дополнений...'], ['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'], + ['sql_editor_output_logs_button_tooltip', 'Показать вывод сервера (Shift + Ctrl + O)'], + ['sql_editor_output_logs_tab_title', 'Вывод логов'], + ['sql_editor_output_logs_input_placeholder', 'Введите часть сообщения для поиска'], ['sql_editor_sql_execution_button_tooltip', 'Выполнить SQL Выражение (Ctrl + Enter)'], ['sql_editor_sql_execution_new_tab_button_tooltip', 'Выполнить SQL Выражение в новой вкладке (Ctrl + \\)(Shift + Ctrl + Enter)'], ['sql_editor_sql_execution_script_button_tooltip', 'Исполнить SQL Скрипт (Alt + X)'], diff --git a/webapp/packages/plugin-sql-editor/src/locales/zh.ts b/webapp/packages/plugin-sql-editor/src/locales/zh.ts index ca1124c0f8..3c6d9ac298 100644 --- a/webapp/packages/plugin-sql-editor/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor/src/locales/zh.ts @@ -7,6 +7,9 @@ export default [ ['sql_editor_placeholder', '使用Ctrl + Enter执行查询以查看结果'], ['sql_editor_hint_empty', 'There is no proposals...'], ['sql_editor_execution_plan_button_tooltip', '解释执行计划(Shift + Ctrl + E)'], + ['sql_editor_output_logs_button_tooltip', 'Show server output (Shift + Ctrl + O)'], + ['sql_editor_output_logs_tab_title', 'Output'], + ['sql_editor_output_logs_input_placeholder', 'Enter a part of a message to search for here'], ['sql_editor_sql_execution_button_tooltip', '执行SQL语句(Ctrl + Enter)'], ['sql_editor_sql_execution_new_tab_button_tooltip', '在新选项卡中执行SQL语句(Ctrl + \\)(Shift + Ctrl + Enter)'], ['sql_editor_sql_execution_script_button_tooltip', '执行SQL脚本(Alt + X)'], diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index 1118d92010..25de7423c0 100644 --- a/webapp/packages/plugin-sql-editor/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor/src/manifest.ts @@ -17,6 +17,10 @@ import { SqlEditorService } from './SqlEditorService'; import { SqlEditorSettingsService } from './SqlEditorSettingsService'; import { SqlEditorView } from './SqlEditorView'; import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService'; +import { OutputMenuBootstrap } from './SqlResultTabs/OutputLogs/OutputMenuBootstrap'; +import { OutputLogsEventHandler } from './SqlResultTabs/OutputLogs/OutputLogsEventHandler'; +import { OutputLogsResource } from './SqlResultTabs/OutputLogs/OutputLogsResource'; +import { OutputLogsService } from './SqlResultTabs/OutputLogs/OutputLogsService'; import { SqlQueryResultService } from './SqlResultTabs/SqlQueryResultService'; import { SqlQueryService } from './SqlResultTabs/SqlQueryService'; import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService'; @@ -40,5 +44,9 @@ export const sqlEditorPluginManifest: PluginManifest = { MenuBootstrap, SqlDataSourceService, LocalStorageSqlDataSourceBootstrap, + OutputLogsEventHandler, + OutputLogsResource, + OutputLogsService, + OutputMenuBootstrap, ], };