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 <teunlao@gmail.com>
Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
DenisSinelnikov
2023-09-20 13:02:29 +03:00
committed by GitHub
co-authored by Dmitry Osipov EvgeniaBzzz
parent a981d37a2d
commit 8a685ef47e
49 changed files with 908 additions and 61 deletions
@@ -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
}
@@ -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
@@ -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<WSOutputLogInfo> 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);
}
}
}
}
}
@@ -94,7 +94,9 @@ public interface DBWServiceSQL extends DBWService {
@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(
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException;
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.DBUtils;
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 class WebSQLProcessor implements WebSessionProvider {
@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 class WebSQLProcessor implements WebSessionProvider {
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 class WebSQLProcessor implements WebSessionProvider {
}
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);
@@ -160,7 +160,9 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> i
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),
@@ -363,16 +363,18 @@ public class WebServiceSQL implements DBWServiceSQL {
@NotNull String sql,
@Nullable String resultId,
@Nullable WebSQLDataFilter filter,
@Nullable WebDataFormat dataFormat)
@Nullable WebDataFormat dataFormat,
boolean readLogs,
@NotNull WebSession webSession)
{
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<String>() {
WebAsyncTaskProcessor<String> 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) {
@@ -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;
@@ -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<React.InputHTMLAttributes<HTMLInputElement>, '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<any, any>,
ref,
@@ -196,6 +198,7 @@ export const InputField: InputFieldType = observer(
<Icon name="copy" viewBox="0 0 32 32" className={styles.icon} />
</div>
)}
{icon && <div data-testid="icon-container" className={styles.customIconContainer}>{icon}</div>}
</div>
{(description || passwordType) && (
<div data-testid="field-description" className={s(styles, { fieldDescription: true, valid: !error, invalid: error })}>
@@ -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
}
@@ -19,9 +19,49 @@ import { useCodemirrorExtensions } from './useCodemirrorExtensions';
import { type IDefaultExtensions, useEditorDefaultExtensions } from './useEditorDefaultExtensions';
export const Editor = observer<IEditorProps & IDefaultExtensions, IEditorRef>(
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)(
@@ -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);
return extensions.current;
}
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 [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];
}
@@ -14,4 +14,5 @@ export interface IDatabaseDataOptions {
catalog?: string;
whereFilter: string;
constraints: SqlDataFilterConstraint[];
readLogs?: boolean;
}
@@ -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;
}
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:#338ecc;}</style></defs><path class="cls-1" d="M20.33,2H6A2,2,0,0,0,4,4V28a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V9.67ZM20,4.5,25.46,10H20ZM26,28H6V4H18v6a2,2,0,0,0,2,2h6Z"/><polygon class="cls-1" points="17 23 17 15 23 19 17 23"/><rect class="cls-1" x="15" y="14" width="2" height="10" transform="translate(35 3) rotate(90)"/></svg>

After

Width:  |  Height:  |  Size: 443 B

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#338ecc;}</style></defs><path class="cls-1" d="M15.25,2H4.5A1.5,1.5,0,0,0,3,3.5v18A1.49,1.49,0,0,0,4.49,23h15A1.49,1.49,0,0,0,21,21.52V7.75ZM15,3.87,19.09,8H15Zm4.5,17.6H4.5V3.5h9V8A1.49,1.49,0,0,0,15,9.5H19.5Z"/><polygon class="cls-1" points="12.75 17.75 12.75 11.75 17.25 14.75 12.75 17.75"/><rect class="cls-1" x="11.25" y="11" width="1.5" height="7.5" transform="translate(26.75 2.75) rotate(90)"/></svg>

After

Width:  |  Height:  |  Size: 526 B

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#338ecc;}</style></defs><path class="cls-1" d="M10.16,1H3A1,1,0,0,0,2,2V14a1,1,0,0,0,1,1H13a1,1,0,0,0,1-1V4.84ZM10,2.25,12.73,5H10ZM13,14H3V2H9V5a1,1,0,0,0,1,1h3Z"/><polygon class="cls-1" points="8.5 11.5 8.5 7.5 11.5 9.5 8.5 11.5"/><rect class="cls-1" x="7.5" y="7" width="1" height="5" transform="translate(17.5 1.5) rotate(90)"/></svg>

After

Width:  |  Height:  |  Size: 456 B

@@ -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<string, any>;
}
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;
@@ -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),
@@ -199,6 +199,7 @@ export class QueryDataSource<TOptions extends IDataQueryOptions = IDataQueryOpti
where: options.whereFilter || undefined,
},
dataFormat: this.dataFormat,
readLogs: options.readLogs,
});
return taskInfo;
@@ -11,7 +11,6 @@ import { s, SContext, StyleRegistry, useS } from '@cloudbeaver/core-blocks';
import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui';
import { IDataContext, useMenu } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SQL_EDITOR_ACTIONS_MENU } from './SQL_EDITOR_ACTIONS_MENU';
import SqlEditorActionsMenuBarStyles from './SqlEditorActionsMenuBar.m.css';
@@ -43,7 +42,6 @@ const registry: StyleRegistry = [
export const SqlEditorActionsMenu = observer<Props>(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 (
<SContext registry={registry}>
@@ -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<ISQLEditorDataPrivate>(
() => ({
@@ -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;
}
@@ -72,6 +72,7 @@ export class SqlEditorService {
resultTabs: observable([]),
executionPlanTabs: observable([]),
statisticsTabs: observable([]),
outputLogsTab: undefined,
currentModeId: undefined,
modeState: observable([]),
});
@@ -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<ITab> {
@@ -27,6 +28,7 @@ export class SqlEditorView extends View<ITab> {
ACTION_UNDO,
ACTION_REDO,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
ACTION_SQL_EDITOR_SHOW_OUTPUT,
ACTION_SAVE,
);
}
@@ -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',
});
@@ -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];
@@ -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');
@@ -0,0 +1 @@
export const OUTPUT_LOGS_TAB_ID = 'output_logs';
@@ -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%;
}
}
@@ -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<Props>(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 (
<ContextMenu className={s(styles, { contextMenu: true })} menu={menu} placement="bottom-end" modal>
<Icon className={s(styles, { icon: true })} name="filter" viewBox="0 0 16 16" />
</ContextMenu>
);
});
@@ -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<ISessionLogEvent, ISessionEvent> {
constructor(sessionEventSource: SessionEventSource) {
super(SessionEventTopic.CbDatabaseOutputLog, sessionEventSource);
}
map(event: CbDatabaseOutputLogEvent) {
return event;
}
}
@@ -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<Props>(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 (
<Container className="theme-background-secondary" parent vertical gap dense>
<OutputLogsToolbar state={state} sqlEditorTabState={sqlEditorTabState}/>
<Group overflow box>
{data && <EditorLoader value={state.resultValue} foldGutter={false} highlightActiveLine={false} lineWrapping readonly />}
</Group>
</Container>
);
});
@@ -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<IOutputLog[]> {
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<IOutputLog[]> {
return this.data;
}
}
@@ -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<void> {
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);
}
}
@@ -0,0 +1,6 @@
.searchIcon {
width: 24px;
height: 24px;
display: flex;
border-radius: var(--theme-menu-bar-small-action-radius);
}
@@ -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<Props>(function SqlOutputLogsToolbar({ state, sqlEditorTabState }) {
const styles = useS(style);
const translate = useTranslate();
return (
<Container noWrap center gap dense keepSize>
<InputField
value={state.searchValue}
placeholder={translate('sql_editor_output_logs_input_placeholder')}
icon={
<div className={s(styles, { searchIcon: true })}>
<Icon name="search" viewBox="0 0 24 24" />
</div>
}
fill
onChange={value => state.setSearchValue(value.toString())}
/>
<Container keepSize>
<OutputLogsFilterMenu sqlEditorTabState={sqlEditorTabState} />
</Container>
</Container>
);
});
@@ -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<void> {
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<void> {}
}
@@ -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<SqlOutputLogsPanelState>(
() => ({
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,
);
@@ -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);
@@ -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<Props>(function SqlResultPanel({ state, i
);
}
if (state.outputLogsTab) {
return styled(style)(
<result-panel>
<OutputLogsPanel sqlEditorTabState={state} />
</result-panel>,
);
}
return null;
});
@@ -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) {
@@ -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',
});
@@ -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,
});
@@ -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';
@@ -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)'],
@@ -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)'],
@@ -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)'],
@@ -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)'],
@@ -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,
],
};