| {item.type} |
{item.time} |
- {item.stackTrace ? {item.message} : item.message}
+ {item.stackTrace ? (
+ onItemSelect(selected ? null : item)}>
+ {item.message}
+
+ ) : item.message}
|
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewer.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewer.tsx
new file mode 100644
index 0000000000..42cf644f24
--- /dev/null
+++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewer.tsx
@@ -0,0 +1,72 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 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 styled, { css } from 'reshadow';
+
+import { Pane, ResizerControls, Split, splitStyles } from '@cloudbeaver/core-blocks';
+import { composes, useStyles } from '@cloudbeaver/core-theming';
+
+import { LogViewerInfoPanel } from './LogViewerInfoPanel';
+import { LogViewerTable } from './LogViewerTable';
+import { useLogViewer } from './useLogViewer';
+
+const styles = composes(
+ css`
+ pane-content {
+ composes: theme-background-surface theme-text-on-surface from global;
+ }
+ `,
+ css`
+ log-view-wrapper, Pane, pane-content {
+ position: relative;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ }
+`);
+
+export const LogViewer = observer(function LogViewer() {
+ const style = useStyles(styles, splitStyles);
+ const logViewerState = useLogViewer();
+
+ if (!logViewerState.isActive) {
+ return null;
+ }
+
+ return styled(style)(
+
+
+
+
+
+
+
+ {logViewerState.selectedItem && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+});
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerInfoPanel.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerInfoPanel.tsx
new file mode 100644
index 0000000000..07a520fbb8
--- /dev/null
+++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerInfoPanel.tsx
@@ -0,0 +1,90 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 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 styled, { css } from 'reshadow';
+
+import { Button, TextareaNew, useClipboard } from '@cloudbeaver/core-blocks';
+import { useTranslate } from '@cloudbeaver/core-localization';
+
+import type { ILogEntry } from './ILogEntry';
+
+const styles = css`
+ panel-wrapper {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ content-wrapper {
+ padding: 0 16px 16px;
+ height: 100%;
+ overflow: auto;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ }
+ buttons {
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
+ buttons Button:first-child {
+ margin-right: 24px;
+ }
+ type {
+ composes: theme-typography--body2 from global;
+ margin: 0;
+ font-weight: 500;
+ }
+ message {
+ overflow: auto;
+ max-height: 96px;
+ }
+ type, message {
+ margin-bottom: 12px;
+ }
+
+`;
+
+interface Props {
+ selectedItem: ILogEntry;
+ onClose: () => void;
+ className?: string;
+}
+
+export const LogViewerInfoPanel: React.FC = function LogViewerInfoPanel({ selectedItem, onClose, className }) {
+ const translate = useTranslate();
+ const copy = useClipboard();
+
+ const valueToCopy = `${selectedItem.message}\n\n${selectedItem.stackTrace}`;
+ const typeInfo = `${selectedItem.type} ${selectedItem.time}`;
+
+ return styled(styles)(
+
+
+
+
+
+
+ {typeInfo}
+ {selectedItem.message}
+
+
+
+ );
+};
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerMenuService.ts
similarity index 98%
rename from webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts
rename to webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerMenuService.ts
index 27d3896e53..1ba1a5025a 100644
--- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts
+++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerMenuService.ts
@@ -38,5 +38,5 @@ export class LogViewerMenuService extends Bootstrap {
this.logViewerService.toggle();
}
- load(): void {}
+ load(): void { }
}
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerService.ts
similarity index 100%
rename from webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts
rename to webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerService.ts
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerTable.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerTable.tsx
new file mode 100644
index 0000000000..a76f9f939b
--- /dev/null
+++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/LogViewerTable.tsx
@@ -0,0 +1,107 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 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 styled, { css, use } from 'reshadow';
+
+import { Button } from '@cloudbeaver/core-blocks';
+import { useTranslate } from '@cloudbeaver/core-localization';
+import { composes, useStyles } from '@cloudbeaver/core-theming';
+
+import type { ILogEntry } from './ILogEntry';
+import { LogEntry } from './LogEntry';
+
+const styles = composes(
+ css`
+ type, timestamp, message {
+ composes: theme-border-color-background from global;
+ }
+ `,
+ css`
+ wrapper {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ table-wrapper {
+ overflow: auto;
+ }
+ buttons {
+ padding: 16px
+ }
+ table {
+ flex: 1 1 auto;
+ width: 100%;
+ table-layout: fixed;
+ }
+ tr {
+ border-top: 1px solid;
+ }
+ type, timestamp, message {
+ box-sizing: border-box;
+ white-space: nowrap;
+ padding: 16px;
+ height: 36px;
+ padding-top: unset;
+ padding-bottom: unset;
+ text-transform: uppercase;
+ text-align: left;
+ text-decoration: none !important;
+ }
+ type {
+ width: 80px;
+ }
+ timestamp {
+ width: 180px;
+ }
+ `
+);
+
+interface Props {
+ items: ILogEntry[];
+ selectedItem: ILogEntry | null;
+ onItemSelect: (item: ILogEntry | null) => void;
+ onClearTable: () => void;
+ className?: string;
+}
+export const LogViewerTable: React.FC = observer(function LogViewerTable({ items, selectedItem, onItemSelect, onClearTable, className }) {
+ const translate = useTranslate();
+ const style = useStyles(styles);
+
+ return styled(style)(
+
+
+
+
+
+
+
+
+ {translate('app_log_view_entry_type')}
+ {translate('app_log_view_entry_timestamp')}
+ {translate('app_log_view_entry_message')}
+
+
+
+ {items.map(item => (
+
+ ))}
+
+
+
+
+ );
+});
diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/useLogViewer.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/useLogViewer.tsx
new file mode 100644
index 0000000000..fdfee541cb
--- /dev/null
+++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewer/useLogViewer.tsx
@@ -0,0 +1,44 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 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 { useState } from 'react';
+
+import { useObjectRef } from '@cloudbeaver/core-blocks';
+import { useService } from '@cloudbeaver/core-di';
+
+import type { ILogEntry } from './ILogEntry';
+import { LogViewerService } from './LogViewerService';
+
+export function useLogViewer() {
+ const logViewerService = useService(LogViewerService);
+ const [selectedItem, setSelectedItem] = useState(null);
+
+ const props = useObjectRef({ selectedItem, logViewerService });
+
+ return useObjectRef({
+ get selectedItem() {
+ return props.selectedItem;
+ },
+ selectItem(item: ILogEntry | null) {
+ setSelectedItem(item);
+ },
+ get isActive() {
+ return props.logViewerService.isActive;
+ },
+ get logItems() {
+ return props.logViewerService.getLog();
+ },
+ closeInfoPanel() {
+ setSelectedItem(null);
+ },
+ clearLog() {
+ props.logViewerService.clearLog();
+ setSelectedItem(null);
+ },
+ }, {});
+}
diff --git a/webapp/packages/core-blocks/src/useClipboard.ts b/webapp/packages/core-blocks/src/useClipboard.ts
index 0219b0cb27..52760399c8 100644
--- a/webapp/packages/core-blocks/src/useClipboard.ts
+++ b/webapp/packages/core-blocks/src/useClipboard.ts
@@ -8,22 +8,23 @@
import { useCallback } from 'react';
+import { useService } from '@cloudbeaver/core-di';
+import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
+import { copyToClipboard } from '@cloudbeaver/core-utils';
+
export function useClipboard() {
- const copy = useCallback((data: string) => {
- const shadowElement = document.createElement('textarea');
- shadowElement.value = data;
- shadowElement.style.position = 'absolute';
- shadowElement.style.overflow = 'hidden';
- shadowElement.style.width = '0';
- shadowElement.style.height = '0';
- shadowElement.style.top = '0';
- shadowElement.style.left = '0';
+ const notificationService = useService(NotificationService);
- document.body.appendChild(shadowElement);
- shadowElement.select();
- document.execCommand('copy');
- document.body.removeChild(shadowElement);
- }, []);
+ const copy = useCallback((value: string, notify = false) => {
+ try {
+ copyToClipboard(value);
+ if (notify) {
+ notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Success);
+ }
+ } catch (exception) {
+ notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy');
+ }
+ }, [notificationService]);
- return [copy];
+ return copy;
}
diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts
index 6e1e3ee20f..14093fb2a8 100644
--- a/webapp/packages/core-localization/src/locales/en.ts
+++ b/webapp/packages/core-localization/src/locales/en.ts
@@ -15,6 +15,7 @@ export const defaultENLocale = [
['ui_processing_save', 'Save'],
['ui_error_message', 'Error:'],
['ui_error_close', 'Close'],
+ ['ui_close', 'Close'],
['ui_errors_details', 'Details'],
['ui_search', 'Search...'],
['ui_delete', 'Delete'],
@@ -22,6 +23,9 @@ export const defaultENLocale = [
['ui_data_delete_confirmation', 'Delete confirmation'],
['ui_no_matches_placeholder', 'Your search returned no matches.'],
['ui_information', 'Information'],
+ ['ui_copy_to_clipboard', 'Copy'],
+ ['ui_copy_to_clipboard_copied', 'Copied'],
+ ['ui_copy_to_clipboard_failed_to_copy', 'Failed to copy'],
['root_permission_denied', 'You have no permissions'],
['app_root_session_expired_title', 'Session expired'],
['app_root_session_expired_message', 'Session has been expired. Do you want to reload?'],
diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts
index 4fabefd7f0..52288ed97e 100644
--- a/webapp/packages/core-localization/src/locales/ru.ts
+++ b/webapp/packages/core-localization/src/locales/ru.ts
@@ -13,6 +13,7 @@ export const defaultRULocale = [
['ui_processing_save', 'Сохранить'],
['ui_error_message', 'Ошибка:'],
['ui_error_close', 'Закрыть'],
+ ['ui_close', 'Закрыть'],
['ui_errors_details', 'Информация'],
['ui_search', 'Поиск...'],
['ui_delete', 'Удалить'],
@@ -20,6 +21,9 @@ export const defaultRULocale = [
['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'],
['ui_data_delete_confirmation', 'Подтвердите удаление'],
['ui_information', 'Информация'],
+ ['ui_copy_to_clipboard', 'Копировать'],
+ ['ui_copy_to_clipboard_copied', 'Скопировано'],
+ ['ui_copy_to_clipboard_failed_to_copy', 'Не удалось скопировать'],
['root_permission_denied', 'Отказано в доступе'],
['app_root_session_expired_title', 'Сессия истекла'],
['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'],
diff --git a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx
index e6c5d2c970..946fc58c27 100644
--- a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx
+++ b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx
@@ -46,9 +46,9 @@ export const ErrorDetailsDialog: DialogComponent = observe
const translate = useTranslate();
- const [copy] = useClipboard();
+ const copy = useClipboard();
const copyHandler = useCallback(
- () => copy(error.textToCopy),
+ () => copy(error.textToCopy, true),
[]
);