Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
serge-rider
2020-05-04 17:12:14 +03:00
21 changed files with 458 additions and 123 deletions
@@ -32,10 +32,15 @@ module.exports = {
data_transfer_dialog_export: 'Export',
data_transfer_dialog_exporting_object: 'Exporting:',
data_transfer_dialog_configuration_title: 'Export configuration',
data_transfer_dialog_preparation: 'We prepare your file for export. Please wait...',
data_transfer_notification_preparation: 'We prepare your file for download. Please wait...',
data_transfer_notification_ready: 'File ready to download',
data_transfer_notification_error: 'Preparation failed or canceled',
ui_stepper_back: 'Back',
ui_processing_loading: 'Loading...',
ui_processing_cancel: 'Cancel',
ui_processing_canceling: 'Canceling...',
ui_processing_retry: 'Retry',
ui_error_message: 'Error:',
ui_errors_details: 'Details',
@@ -31,10 +31,15 @@ module.exports = {
data_transfer_dialog_export: 'Экспортировать',
data_transfer_dialog_exporting_object: 'Экспортировать:',
data_transfer_dialog_configuration_title: 'Настройка экспорта',
data_transfer_dialog_preparation: 'Подготавливаем файл к экспорту. Пожалуйста подождите...',
data_transfer_notification_preparation: 'Подготавливаем файл к загрузке. Пожалуйста подождите...',
data_transfer_notification_ready: 'Файл готов к загрузке',
data_transfer_notification_error: 'Подготовка файла к загрузке не удалась или была отменена',
ui_stepper_back: 'Назад',
ui_processing_loading: 'Загрузка...',
ui_processing_cancel: 'Отменить',
ui_processing_canceling: 'Отмена...',
ui_processing_retry: 'Повторить',
ui_error_message: 'Ошибка:',
ui_errors_details: 'Информация',
@@ -20,18 +20,18 @@ const FadeTimeout = 5000;
export class NotificationItemController implements IInitializableController {
closeAfter = 0;
@observable isDetailsDialogOpen = false;
private notification!: INotification;
private notification!: INotification<any>;
constructor(private commonDialogService: CommonDialogService) {}
init(notification: INotification) {
init(notification: INotification<any>) {
this.notification = notification;
if (this.notification.details) {
this.handleShowDetails = this.showDetails.bind(this);
}
if (notification.type !== ENotificationType.Error) {
if (notification.type !== ENotificationType.Error && !notification.persistent) {
this.closeAfter = FadeTimeout;
}
}
@@ -15,7 +15,7 @@ import { NotificationItemController } from './NotificationItemController';
import { Snackbar } from './Snackbar/Snackbar';
type NotificationProps = {
notification: INotification;
notification: INotification<any>;
}
export const NotificationsItem = observer(function Notification({ notification }: NotificationProps) {
@@ -24,6 +24,11 @@ export const NotificationsItem = observer(function Notification({ notification }
}
const controller = useController(NotificationItemController, notification);
if (notification.customComponent) {
const Custom = notification.customComponent();
return <Custom notification={notification} onClose={controller.handleClose} />;
}
return (
<Snackbar
text={notification.title}
@@ -12,27 +12,33 @@ import styled, { use } from 'reshadow';
import { Icon, Button } from '@dbeaver/core/blocks';
import { ENotificationType } from '@dbeaver/core/eventsLog';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { NotificationMark } from './NotificationMark';
import { snackbarStyles } from './styles';
import { SNACKBAR_STYLES } from './styles';
type SnackbarProps = {
type?: ENotificationType;
text: string;
closeAfter?: number;
type?: ENotificationType;
disableShowDetails?: boolean;
onClose?: () => void;
onShowDetails?: () => void;
}
export function Snackbar(props: SnackbarProps) {
const styles = useStyles(snackbarStyles);
export function Snackbar({
type,
text,
closeAfter,
disableShowDetails,
onClose,
onShowDetails,
}: SnackbarProps) {
const styles = useStyles(SNACKBAR_STYLES);
const [mounted, setMounted] = useState(false);
const [closing, setClosing] = useState(false);
const {
closeAfter, onClose, onShowDetails, type, text,
} = props;
const translate = useTranslate();
useEffect(() => {
setMounted(true);
@@ -72,9 +78,9 @@ export function Snackbar(props: SnackbarProps) {
type="button"
mod={['outlined']}
onClick={onShowDetails}
disabled={props.disableShowDetails}
disabled={disableShowDetails}
>
Details
{translate('ui_errors_details')}
</Button>
</actions>
)}
@@ -10,7 +10,7 @@ import { css } from 'reshadow';
import { composes } from '@dbeaver/core/theming';
export const snackbarStyles = composes(
export const SNACKBAR_STYLES = composes(
css`
notification {
composes: theme-background-surface theme-text-on-surface from global;
+2
View File
@@ -18,6 +18,8 @@ export * from './NavigationTree/NavigationTreeService';
export * from './NavigationTree/NavigationTreeContextMenuService';
export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService';
export * from './shared/ToolsPanel/LogViewTab/LogViewerService';
export * from './Notifications/NotificationsItem/Snackbar/styles';
export * from './Notifications/NotificationsItem/Snackbar/NotificationMark';
// Models
export * from './shared/NavigationTabs/ITab';
@@ -38,12 +38,25 @@ export type DBSource = Pick<DataSourceInfo, 'id' | 'name' | 'driverId' | 'descri
export type Connection = Pick<ConnectionInfo, 'id' | 'name' | 'connected' | 'driverId'>
export type ObjectContainer = Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>
type DBDriversMetadata = {
loaded: boolean;
}
@injectable()
export class ConnectionsManagerService {
@observable private connectionsMap: Map<string, Connection> = new Map();
readonly dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
readonly connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this));
readonly dbDrivers = new CachedResource(
new Map(),
this.refreshDriversAsync.bind(this),
(_, { loaded }) => loaded,
{ loaded: false }
);
readonly connectionObjectContainers = new CachedResource(
new Map(),
this.refreshObjectContainersAsync.bind(this),
this.isObjectContainersLoaded.bind(this)
);
@computed get connections(): Connection[] {
return Array.from(this.connectionsMap.values());
@@ -129,24 +142,36 @@ export class ConnectionsManagerService {
await this.nodesManagerService.updateRootChildren();
}
private isObjectContainersLoaded(
data: Map<string, ObjectContainer[]>,
metadata: {},
connectionId: string,
catalogId?: string,
) {
return data.has(connectionId);
}
private async refreshObjectContainersAsync(
data: Map<string, ObjectContainer[]>,
refresh: boolean,
metadata: {},
update: boolean,
connectionId: string,
catalogId?: string,
): Promise<Map<string, ObjectContainer[]>> {
if (refresh || !data.has(connectionId)) {
const { navGetStructContainers } = await this.graphQLService.gql.navGetStructContainers({
connectionId,
catalogId,
});
data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]);
}
const { navGetStructContainers } = await this.graphQLService.gql.navGetStructContainers({
connectionId,
catalogId,
});
data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]);
return data;
}
private async refreshDriversAsync(data: Map<string, DBDriver>): Promise<Map<string, DBDriver>> {
private async refreshDriversAsync(
data: Map<string, DBDriver>,
metadata: DBDriversMetadata,
update: boolean
): Promise<Map<string, DBDriver>> {
const { driverList } = await this.graphQLService.gql.driverList();
data.clear();
@@ -154,7 +179,7 @@ export class ConnectionsManagerService {
for (const driver of driverList) {
data.set(driver.id, driver);
}
metadata.loaded = true;
return data;
}
+30 -13
View File
@@ -12,52 +12,69 @@ import { useStyles, composes } from '@dbeaver/core/theming';
const buttonStyles = composes(
css`
button {
composes: theme-button from global;
}
ripple {
composes: theme-button_ripple from global;
}
Button {
composes: theme-button from global;
}
ripple {
composes: theme-button_ripple from global;
}
`,
css`
Button {
display: flex;
}
`
);
const buttonMod = {
raised: composes(
css`
button {
Button {
composes: theme-button_raised from global;
}
`
),
unelevated: composes(
css`
button {
Button {
composes: theme-button_unelevated from global;
}
`
),
outlined: composes(
css`
button {
Button {
composes: theme-button_outlined from global;
}
`
),
secondary: composes(
css`
button {
Button {
composes: theme-button_secondary from global;
}
`
),
};
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
type ButtonProps = (
React.ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement>
& React.LinkHTMLAttributes<HTMLLinkElement | HTMLButtonElement>
) & {
mod?: (keyof typeof buttonMod)[];
tag?: 'button' | 'a';
href?: string;
download?: boolean;
}
export function Button({ children, mod, ...rest }: ButtonProps) {
export function Button({
children,
mod,
tag = 'button',
...rest
}: ButtonProps) {
const Button = tag;
return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))(
<button {...rest}><ripple as="div"/>{children}</button>
<Button {...rest}><ripple as="div"/>{children}</Button>
);
}
@@ -20,6 +20,8 @@ export type LoaderProps = {
loading?: boolean;
/** disable cancel button */
cancelDisabled?: boolean;
/** hides message */
hideMessage?: boolean;
/** render loader as overlay with white spinner */
overlay?: boolean;
/** loader with white spinner */
@@ -38,6 +40,7 @@ const spinnerType = {
export function Loader({
cancelDisabled,
overlay,
hideMessage,
secondary,
small,
className,
@@ -66,7 +69,7 @@ export function Loader({
return styled(useStyles(loaderStyles, ...(overlay ? [overlayStyles] : [])))(
<loader as="div" className={className} {...use({ small })}>
<icon as="div"><img src={spinnerURL}/></icon>
<message as="div"><Translate token='ui_processing_loading' /></message>
{!hideMessage && <message as="div"><Translate token='ui_processing_loading' /></message>}
{onCancel && (
<actions as='div'>
<Button type="button" mod={['unelevated']}
@@ -11,20 +11,32 @@ export enum ENotificationType {
Error = 'Error',
}
export interface INotification {
export type NotificationComponentProps<T = undefined> = {
notification: INotification<T>;
onClose: () => void;
}
export type NotificationComponent<T = undefined> = React.FunctionComponent<NotificationComponentProps<T>>
export interface INotification<T = undefined> {
readonly id: number;
type: ENotificationType;
title: string;
message?: string;
details?: string | Error;
persistent: boolean;
isSilent: boolean;
customComponent?: () => NotificationComponent<T>;
source: T;
close: () => void;
showDetails: () => void;
}
export interface INotificationOptions {
export interface INotificationOptions<T = undefined> {
title: string;
message?: string;
details?: string | Error;
isSilent?: boolean;
persistent?: boolean;
customComponent?: () => NotificationComponent<T>;
source?: T;
}
@@ -10,44 +10,47 @@ import { computed } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { GQLError, ServerInternalError } from '@dbeaver/core/sdk';
import { OrderedMap } from '@dbeaver/core/utils';
import { EntityList } from '@dbeaver/core/utils';
import { ENotificationType, INotification, INotificationOptions } from './INotification';
@injectable()
export class NotificationService {
private notificationList = new OrderedMap<number, INotification>();
private notificationList = new EntityList<number, INotification<any>>(({ id }) => id);
private notificationNextId = 0
/**
* By default filtered without silent notifications
*/
@computed get notifications(): INotification[] {
@computed get notifications(): INotification<any>[] {
return this.notificationList.values
.filter(notification => !notification.isSilent);
}
notify(options: INotificationOptions, type: ENotificationType) {
notify<T = never>(options: INotificationOptions<T>, type: ENotificationType) {
const id = this.notificationNextId++;
const notification: INotification = {
const notification: INotification<T> = {
id,
title: options.title,
message: options.message,
details: options.details,
isSilent: !!options.isSilent,
persistent: !!options.persistent,
customComponent: options.customComponent,
source: options.source!,
type,
close: this.close.bind(this, id),
showDetails: this.showDetails.bind(this, id),
};
this.notificationList.add(notification.id, notification);
this.notificationList.set(notification);
}
logInfo(notification: INotificationOptions) {
logInfo<T>(notification: INotificationOptions<T>) {
this.notify(notification, ENotificationType.Info);
}
logError(notification: INotificationOptions) {
logError<T>(notification: INotificationOptions<T>) {
this.notify(notification, ENotificationType.Error);
}
@@ -15,7 +15,7 @@ import { SessionService } from './SessionService';
@injectable()
export class ServerService {
readonly config = new CachedResource(undefined, this.refreshConfigAsync.bind(this));
readonly config = new CachedResource(undefined, this.refreshConfigAsync.bind(this), data => !!data);
readonly settings = new ServerSettingsService(this.sessionService.settings);
private lastConfig: any = null
@@ -26,10 +26,6 @@ export class ServerService {
}
private async refreshConfigAsync(data: ServerConfig | undefined): Promise<ServerConfig> {
if (data) {
return data;
}
const { serverConfig } = await this.graphQLService.gql.serverConfig();
if (serverConfig.productConfiguration !== this.lastConfig) {
@@ -19,16 +19,12 @@ export type SessionState = Pick<SessionInfo, 'id' | 'createTime' | 'cacheExpired
@injectable()
export class SessionService {
readonly session = new CachedResource(undefined, this.refreshSessionStateAsync.bind(this));
readonly session = new CachedResource(undefined, this.refreshSessionStateAsync.bind(this), data => !!data);
readonly settings = new SessionSettingsService('session_settings');
constructor(private graphQLService: GraphQLService) {}
private async refreshSessionStateAsync(data: SessionState | undefined): Promise<SessionState> {
if (data) {
return data;
}
const { session } = await this.graphQLService.gql.openSession();
return session;
+46 -48
View File
@@ -8,24 +8,42 @@
import { observable } from 'mobx';
export type Loader<TData, TArgs extends any[]> = (current: TData, update: boolean, ...args: TArgs) => Promise<TData>
export type Loader<TData, TMetadata = {}, TArgs extends any[] = []> = (
current: TData,
metadata: TMetadata,
update: boolean,
...args: TArgs
) => Promise<TData>
export class CachedResource<TData, TArgs extends any[] = []> {
export type IsLoaded<TData, TMetadata = {}, TArgs extends any[] = []> = (
current: TData,
metadata: TMetadata,
...args: TArgs
) => boolean
export class CachedResource<TData, TMetadata = {}, TArgs extends any[] = []> {
@observable data: TData;
@observable private loaded = false;
@observable private loading = false;
private refreshPromise: Promise<TData> | null = null;
private singleElementPromise: Promise<TData> | null = null;
private loader: Loader<TData, TArgs>;
@observable private metadata: TMetadata;
private loader: Loader<TData, TMetadata, TArgs>;
private isLoadedCheck: IsLoaded<TData, TMetadata, TArgs>;
private activePromise: Promise<TData> | null = null;
constructor(defaultValue: TData, loader: Loader<TData, TArgs>) {
constructor(
defaultValue: TData,
loader: Loader<TData, TMetadata, TArgs>,
isLoadedCheck: IsLoaded<TData, TMetadata, TArgs>,
metadata?: TMetadata,
) {
this.data = defaultValue;
this.loader = loader;
this.isLoadedCheck = isLoadedCheck;
this.metadata = metadata || {} as TMetadata;
}
isLoaded(): boolean {
return this.loaded;
isLoaded(...args: TArgs): boolean {
return this.isLoadedCheck(this.data, this.metadata, ...args);
}
isLoading(): boolean {
@@ -33,64 +51,44 @@ export class CachedResource<TData, TArgs extends any[] = []> {
}
async refresh(...args: TArgs): Promise<TData> {
if (args.length > 0) {
await this.loadSingle(true, args);
} else {
await this.loadAll(true, args);
}
return this.data;
return this.loadAll(true, args);
}
async load(...args: TArgs): Promise<TData> {
if (!this.loaded) {
if (args.length > 0) {
await this.loadSingle(false, args);
} else {
await this.loadAll(false, args);
}
}
return this.data;
return this.loadAll(false, args);
}
private async loadAll(update: boolean, args: TArgs) {
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.loadingTask(update, args);
await this.waitActive();
this.activePromise = this.loadingTask(update, args);
try {
await this.refreshPromise;
return await this.activePromise;
} finally {
this.refreshPromise = null;
}
}
private async loadSingle(update: boolean, args: TArgs) {
if (this.singleElementPromise) {
return this.singleElementPromise;
}
this.singleElementPromise = this.loadingTask(update, args);
try {
await this.singleElementPromise;
} finally {
this.singleElementPromise = null;
this.activePromise = null;
}
}
private async loadingTask(update: boolean, args: TArgs): Promise<TData> {
if (args.length === 0) {
this.loaded = false;
}
this.loading = true;
try {
this.data = await this.loader(this.data, update, ...args);
if (this.isLoaded(...args) && !update) {
return this.data;
}
this.data = await this.loader(this.data, this.metadata, update, ...args);
} finally {
this.loading = false;
}
if (args.length === 0) {
this.loaded = true;
}
return this.data;
}
private async waitActive() {
if (this.activePromise) {
try {
await this.activePromise;
} catch {}
}
}
}
@@ -0,0 +1,54 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 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';
export class EntityList<K, V> {
private identifiers = observable.array<K>([]);
@observable private map: Map<K, V> = new Map<K, V>();
@computed get keys(): K[] {
return this.identifiers;
}
@computed get values(): V[] {
return this.identifiers.map(i => this.map.get(i)!);
}
constructor(private keySelector: (val: V) => K) { }
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
@action
set(value: V): void {
const key = this.keySelector(value);
this.identifiers.push(key);
this.map.set(key, value);
}
@action
remove(key: K): void {
if (!this.map.has(key)) {
return;
}
this.map.delete(key);
this.identifiers.remove(key);
}
@action
clear(): void {
this.identifiers.clear();
this.map.clear();
}
}
+1
View File
@@ -1,6 +1,7 @@
export * from './OrderedMap';
export * from './uuid';
export * from './parseJSONFlat';
export * from './EntityList';
export * from './Promises/CancellablePromise';
export * from './Promises/Deferred';
@@ -6,7 +6,9 @@
* you may not use this file except in compliance with the License.
*/
import { ErrorDetailsDialog } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import {
CachedResource, GraphQLService, DataTransferProcessorInfo, DataTransferParameters
@@ -15,15 +17,25 @@ import { Deferred } from '@dbeaver/core/utils';
import { ExportFromContainerProcess } from './ExportFromContainerProcess';
import { ExportFromResultsProcess } from './ExportFromResultsProcess';
import { PendingNotification } from './PendingNotification';
type ProcessorsResourceMetadata = {
loaded: boolean;
}
@injectable()
export class DataExportService {
readonly processors = new CachedResource(new Map(), this.refreshProcessorsAsync.bind(this));
private exportProcesses = new Map<string, Deferred<string>>();
readonly processors = new CachedResource(
new Map(),
this.refreshProcessorsAsync.bind(this),
(_, { loaded }) => loaded
);
readonly exportProcesses = new Map<string, Deferred<string>>();
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService
private notificationService: NotificationService,
private commonDialogService: CommonDialogService,
) { }
async cancel(exportId: string) {
@@ -46,7 +58,19 @@ export class DataExportService {
try {
await this.graphQLService.gql.removeDataTransferFile({ dataFileId });
this.exportProcesses.delete(exportId);
} catch (e) {
} catch (exception) {
this.notificationService.logException(exception, 'Error occurred while deleting file');
}
}
async showDetails(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
try {
await this.commonDialogService.open(ErrorDetailsDialog, process.getRejectionReason());
} finally {
}
}
@@ -60,6 +84,17 @@ export class DataExportService {
return;
}
this.exportProcesses.delete(exportId);
}
downloadUrl(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
return `/dbeaver/data/${dataFileId}`;
}
@@ -72,9 +107,7 @@ export class DataExportService {
const taskId = await process.start(connectionId, containerNodePath, parameters);
if (taskId) {
this.exportProcesses.set(taskId, process);
// this.notificationService.logInfo({ title: 'We prepare your file for export. Please wait' });
await process.promise;
window.open(this.download(taskId), '_blank');
this.showPendingNotification(taskId);
return taskId;
}
}
@@ -89,14 +122,22 @@ export class DataExportService {
const taskId = await process.start(connectionId, contextId, resultsId, parameters);
if (taskId) {
this.exportProcesses.set(taskId, process);
await process.promise;
window.open(this.download(taskId), '_blank');
this.showPendingNotification(taskId);
return taskId;
}
}
private showPendingNotification(taskId: string) {
this.notificationService.logInfo({
title: 'We prepare your file for export. Please wait',
source: taskId,
customComponent: () => PendingNotification,
});
}
private async refreshProcessorsAsync(
data: Map<string, DataTransferProcessorInfo>
data: Map<string, DataTransferProcessorInfo>,
metadata: ProcessorsResourceMetadata,
): Promise<Map<string, DataTransferProcessorInfo>> {
const { processors } = await this.graphQLService.gql.getDataTransferProcessors();
@@ -105,7 +146,7 @@ export class DataExportService {
for (const processor of processors) {
data.set(processor.id, processor);
}
metadata.loaded = true;
return data;
}
}
@@ -38,7 +38,7 @@ export class DataExportController implements IInitializableController {
.sort((a, b) => this.sortProcessors(a, b));
}
@observable readonly processorProperties: any = {}
@observable processorProperties: any = {}
@observable properties: IProperty[] = []
private context!: IExportContext;
@@ -108,6 +108,9 @@ export class DataExportController implements IInitializableController {
validValues: property.validValues,
defaultValue: property.defaultValue,
})) || [];
this.processorProperties = {};
this.step = DataExportStep.Configure;
}
@@ -9,7 +9,7 @@
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { Loader, IProperty, PropertiesTable } from '@dbeaver/core/blocks';
import { IProperty, PropertiesTable } from '@dbeaver/core/blocks';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
@@ -49,7 +49,8 @@ export const ProcessorConfigureDialog = observer(
onBack,
onExport,
}: ProcessorSelectDialogProps) {
const title = `${useTranslate('data_transfer_dialog_configuration_title')} (${processor.name})`;
const translate = useTranslate();
const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`;
return styled(styles)(
<CommonDialogWrapper
@@ -65,7 +66,7 @@ export const ProcessorConfigureDialog = observer(
}
onReject={onClose}
>
{isExporting && <message as="div">We prepare your file for export. Please wait...</message>}
{isExporting && <message as="div">{translate('data_transfer_dialog_preparation')}</message>}
{!isExporting && (
<PropertiesTable
properties={properties}
@@ -0,0 +1,162 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 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';
import {
useEffect, useState, useCallback
} from 'react';
import { Button as ReakitButton } from 'reakit/Button';
import styled, { use, css } from 'reshadow';
import { SNACKBAR_STYLES } from '@dbeaver/core/app';
import { Icon, Button, Loader } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { NotificationComponentProps } from '@dbeaver/core/eventsLog';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { EDeferredState } from '@dbeaver/core/utils';
import { DataExportService } from './DataExportService';
const styles = css`
Loader {
margin-right: 16px;
}
actions {
display: flex;
}
fill {
flex: 1;
}
Button {
margin-left: 16px !important;
}
actions Button {
display: flex;
}
`;
export const PendingNotification = observer(function PendingNotification({
notification,
onClose,
}: NotificationComponentProps<string>) {
const translate = useTranslate();
const dataExportService = useService(DataExportService);
const task = dataExportService.exportProcesses.get(notification.source);
if (!task) {
return null;
}
const [mounted, setMounted] = useState(false);
const [idDetailsOpen, setDetails] = useState(false);
const getDownloadUrl = useCallback(
() => dataExportService.downloadUrl(notification.source),
[dataExportService, notification]
);
const handleDelete = useCallback(() => {
dataExportService.delete(notification.source);
onClose();
}, [dataExportService, notification, onClose]);
const handleDownload = useCallback(() => {
dataExportService.download(notification.source);
onClose();
}, [dataExportService, notification, onClose]);
const handleCancel = useCallback(() => {
dataExportService.cancel(notification.source);
}, [dataExportService, notification]);
const handleDetails = useCallback(async () => {
try {
setDetails(true);
await dataExportService.showDetails(notification.source);
} finally {
setDetails(false);
}
}, [dataExportService, notification]);
useEffect(() => {
setMounted(true);
}, []);
const status = useCallback(() => {
switch (task.getState()) {
case EDeferredState.PENDING:
return translate('data_transfer_notification_preparation');
case EDeferredState.CANCELLING:
return translate('ui_processing_canceling');
case EDeferredState.RESOLVED:
return translate('data_transfer_notification_ready');
default:
return translate('data_transfer_notification_error');
}
}, [task]);
const isSuccess = task.getState() === EDeferredState.RESOLVED;
return styled(useStyles(SNACKBAR_STYLES, styles))(
<notification as="div" {...use({ mounted })} >
<notification-header as="div">
<Loader loading={task.isInProgress} hideMessage />
<message as="div">{status()}</message>
{task.isFinished && (
<ReakitButton onClick={handleDelete}>
<Icon name="cross" viewBox="0 0 16 16" />
</ReakitButton>
)}
</notification-header>
<notification-body as="div">
<actions as="div">
<fill as="div" />
{task.isFinished && isSuccess && (
<>
<Button
type="button"
mod={['outlined']}
onClick={handleDelete}
>
Delete
</Button>
<Button
type="button"
tag='a'
href={getDownloadUrl()}
mod={['unelevated']}
onClick={handleDownload}
download
>
Download
</Button>
</>
)}
{task.isFinished && !isSuccess && (
<Button
type="button"
mod={['outlined']}
onClick={handleDetails}
disabled={idDetailsOpen}
>
{translate('ui_errors_details')}
</Button>
)}
{!task.isFinished && (
<Button
type="button"
mod={['outlined']}
onClick={handleCancel}
>
{translate('ui_processing_cancel')}
</Button>
)}
</actions>
</notification-body>
</notification>
);
});