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