mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core-app): CB-868 add log viewer info panel
This commit is contained in:
@@ -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()} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
-31
@@ -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;
|
||||
}
|
||||
}
|
||||
-45
@@ -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) {
|
||||
}
|
||||
}
|
||||
+14
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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);
|
||||
},
|
||||
}, {});
|
||||
}
|
||||
@@ -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', 'Сессия истекла. Хотите перезагрузить страницу?'],
|
||||
|
||||
+2
-2
@@ -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),
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user