feat(core-app): CB-868 add log viewer info panel

This commit is contained in:
Naumov Alexey
2021-04-14 14:57:11 +03:00
parent 5dbff80253
commit 4371a40d04
18 changed files with 364 additions and 217 deletions
@@ -12,13 +12,13 @@ import styled, { css } from 'reshadow';
import {
Pane, ResizerControls, SlideBox, SlideElement, slideBoxStyles, Split, splitHorizontalStyles, splitStyles, SlideOverlay
} from '@cloudbeaver/core-blocks';
import { useController, useService } from '@cloudbeaver/core-di';
import { useService } from '@cloudbeaver/core-di';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { NavigationTabsBar } from '../shared/NavigationTabs/NavigationTabsBar';
import { LogViewTab } from '../shared/ToolsPanel/LogViewTab/LogViewTab';
import { LogViewTabController } from '../shared/ToolsPanel/LogViewTab/LogViewTabController';
import { LogViewer } from '../shared/ToolsPanel/LogViewer/LogViewer';
import { useLogViewer } from '../shared/ToolsPanel/LogViewer/useLogViewer';
const styles = composes(
css`
@@ -45,7 +45,7 @@ const styles = composes(
);
export const RightArea = observer(function RightArea() {
const controller = useController(LogViewTabController);
const logViewerState = useLogViewer();
const optionsPanelService = useService(OptionsPanelService);
const OptionsPanel = optionsPanelService.getPanelComponent();
@@ -55,13 +55,13 @@ export const RightArea = observer(function RightArea() {
<OptionsPanel />
</SlideElement>
<SlideElement>
<Split sticky={30} split="horizontal" mode={controller.isActive ? undefined : 'minimize'} keepRatio>
<Split sticky={30} split="horizontal" mode={logViewerState.isActive ? undefined : 'minimize'} keepRatio>
<Pane>
<NavigationTabsBar />
</Pane>
{controller.isActive && <ResizerControls />}
{logViewerState.isActive && <ResizerControls />}
<Pane main>
<LogViewTab />
<LogViewer />
</Pane>
</Split>
<SlideOverlay onClick={() => optionsPanelService.close()} />
+3 -2
View File
@@ -19,8 +19,9 @@ export * from './shared/NodesManager/NavNodeExtensionsService';
export * from './shared/NodesManager/NodeManagerUtils';
export * from './shared/NavigationTabs/NavigationTabsService';
export * from './shared/NavigationTabs/TabNavigationContext';
export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService';
export * from './shared/ToolsPanel/LogViewTab/LogViewerService';
export * from './shared/ToolsPanel/LogViewer/LogViewerMenuService';
export * from './shared/ToolsPanel/LogViewer/LogViewerService';
export * from './shared/SessionExpireDialog/SessionExpiredDialogService';
export * from './NavigationTree/ElementsTree';
export * from './NavigationTree/TreeContext';
@@ -1,31 +0,0 @@
/*
* 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 { IInitializableController, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import type { ILogEntry } from '../ILogEntry';
import { LogEntryDetailsDialog } from './LogEntryDetailsDialog';
@injectable()
export class LogEntryController implements IInitializableController {
get hasDetails() {
return Boolean(this.item.stackTrace);
}
showDetails = () => this.commonDialogService.open(LogEntryDetailsDialog, this.item);
private item!: ILogEntry;
constructor(private commonDialogService: CommonDialogService) {
}
init(item: ILogEntry) {
this.item = item;
}
}
@@ -1,45 +0,0 @@
/*
* 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 { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { useStyles } from '@cloudbeaver/core-theming';
import type { ILogEntry } from '../ILogEntry';
const styles = css`
message {
width: 700px;
max-height: 80px;
margin-bottom: 18px;
overflow: auto;
}
textarea {
width: 100% !important;
min-height: 240px;
box-sizing: border-box;
}
`;
export const LogEntryDetailsDialog: DialogComponent<ILogEntry, null> = observer(
function LogEntryDetailsDialog(props: DialogComponentProps<ILogEntry, null>) {
return styled(useStyles(styles))(
<CommonDialogWrapper
title={`${props.payload.type} ${props.payload.time}`}
onReject={props.rejectDialog}
>
<message as="div">
{props.payload.message}
</message>
<textarea value={props.payload.stackTrace} readOnly />
</CommonDialogWrapper>
);
}
);
@@ -1,75 +0,0 @@
/*
* 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 { Button } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { LogEntry } from './LogEntry/LogEntry';
import { LogViewTabController } from './LogViewTabController';
const styles = css`
wrapper {
display: flex;
flex: 1 1 auto;
flex-direction: column
}
buttons {
padding: 16px
}
table {
flex: 1 1 auto;
width: 100%;
table-layout: fixed;
}
tr {
border-top: 1px solid;
}
.type {
width: 80px;
}
.timestamp {
width: 180px;
}
`;
export const LogViewTab = observer(function LogViewTab() {
const style = useStyles(styles);
const controller = useController(LogViewTabController);
const translate = useTranslate();
if (!controller.isActive) {
return null;
}
return styled(style)(
<wrapper as="div">
<buttons as="div">
<Button mod={['unelevated']} onClick={controller.clearLog}>
{translate('app_log_view_clear_log')}
</Button>
</buttons>
<table>
<thead>
<tr>
<th className={styles.type}>{translate('app_log_view_entry_type')}</th>
<th className={styles.timestamp}>{translate('app_log_view_entry_timestamp')}</th>
<th className={styles.message}>{translate('app_log_view_entry_message')}</th>
</tr>
</thead>
<tbody>
{controller.log.map(item => <LogEntry key={item.id} item={item} />)}
</tbody>
</table>
</wrapper>
);
});
@@ -1,27 +0,0 @@
/*
* 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 { injectable } from '@cloudbeaver/core-di';
import { LogViewerService } from './LogViewerService';
@injectable()
export class LogViewTabController {
get isActive(): boolean {
return this.logViewerService.isActive;
}
get log() {
return this.logViewerService.getLog();
}
clearLog = () => this.logViewerService.clearLog();
constructor(private logViewerService: LogViewerService) {
}
}
@@ -7,17 +7,18 @@
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import styled, { css, use } from 'reshadow';
import { Link } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import type { ILogEntry } from '../ILogEntry';
import { LogEntryController } from './LogEntryController';
import type { ILogEntry } from './ILogEntry';
export interface LogEntryProps {
item: ILogEntry;
onItemSelect: (error: ILogEntry | null) => void;
selected?: boolean;
className?: string;
}
const style = css`
@@ -39,23 +40,24 @@ const style = css`
Link:hover {
cursor: pointer;
}
tr:hover Icon {
fill: #338fcc;
tr[|selected] {
font-weight: 500;
}
`;
export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) {
const controller = useController(LogEntryController, item);
export const LogEntry = observer(function LogEntry({ item, onItemSelect, selected = false, className }: LogEntryProps) {
return styled(useStyles(style))(
<tr>
<tr className={className} {...use({ selected, expanded: selected })}>
<td>{item.type}</td>
<td>{item.time}</td>
<td>
<message-cell as="div">
<message as="div" title={item.message}>
{item.stackTrace ? <Link onClick={controller.showDetails}>{item.message}</Link> : item.message}
{item.stackTrace ? (
<Link onClick={() => onItemSelect(selected ? null : item)}>
{item.message}
</Link>
) : item.message}
</message>
</message-cell>
</td>
@@ -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)(
<log-view-wrapper as='div'>
<Split mode={logViewerState.selectedItem ? undefined : 'maximize'} keepRatio>
<Pane main>
<pane-content as='div'>
<LogViewerTable
items={logViewerState.logItems}
selectedItem={logViewerState.selectedItem}
onClearTable={logViewerState.clearLog}
onItemSelect={logViewerState.selectItem}
/>
</pane-content>
</Pane>
{logViewerState.selectedItem && (
<>
<ResizerControls />
<Pane>
<pane-content as='div'>
<LogViewerInfoPanel
selectedItem={logViewerState.selectedItem}
onClose={logViewerState.closeInfoPanel}
/>
</pane-content>
</Pane>
</>
)}
</Split>
</log-view-wrapper>
);
});
@@ -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<Props> = 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)(
<panel-wrapper as='div' className={className}>
<buttons as='div'>
<Button mod={['unelevated']} onClick={() => copy(valueToCopy, true)}>
{translate('ui_copy_to_clipboard')}
</Button>
<Button mod={['outlined']} onClick={onClose}>
{translate('ui_close')}
</Button>
</buttons>
<content-wrapper as='div'>
<type as='h2'>{typeInfo}</type>
<message as='div' title={selectedItem.message}>{selectedItem.message}</message>
<TextareaNew
name="value"
rows={3}
value={selectedItem.stackTrace}
readOnly
embedded
/>
</content-wrapper>
</panel-wrapper>
);
};
@@ -38,5 +38,5 @@ export class LogViewerMenuService extends Bootstrap {
this.logViewerService.toggle();
}
load(): void {}
load(): void { }
}
@@ -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<Props> = observer(function LogViewerTable({ items, selectedItem, onItemSelect, onClearTable, className }) {
const translate = useTranslate();
const style = useStyles(styles);
return styled(style)(
<wrapper as='div' className={className}>
<buttons as="div">
<Button mod={['unelevated']} onClick={onClearTable}>
{translate('app_log_view_clear_log')}
</Button>
</buttons>
<table-wrapper as="div">
<table {...use({ expanded: selectedItem })}>
<thead>
<tr>
<type as='th'>{translate('app_log_view_entry_type')}</type>
<timestamp as='th'>{translate('app_log_view_entry_timestamp')}</timestamp>
<message as='th'>{translate('app_log_view_entry_message')}</message>
</tr>
</thead>
<tbody>
{items.map(item => (
<LogEntry
key={item.id}
item={item}
selected={item.id === selectedItem?.id}
onItemSelect={onItemSelect}
/>
))}
</tbody>
</table>
</table-wrapper>
</wrapper>
);
});
@@ -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<ILogEntry | null>(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);
},
}, {});
}
+16 -15
View File
@@ -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;
}
@@ -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?'],
@@ -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', 'Сессия истекла. Хотите перезагрузить страницу?'],
@@ -46,9 +46,9 @@ export const ErrorDetailsDialog: DialogComponent<Error | string, null> = observe
const translate = useTranslate();
const [copy] = useClipboard();
const copy = useClipboard();
const copyHandler = useCallback(
() => copy(error.textToCopy),
() => copy(error.textToCopy, true),
[]
);