From f0ec862f93a6ff3273cc4a5fd2915d3ae5e23084 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 6 May 2020 17:30:11 +0300 Subject: [PATCH 1/9] feat(data-export-plugin): some improvements dbeaver-corp/dbeaver-web#618 --- webapp/packages/core/assets/locales/en.js | 5 +- webapp/packages/core/assets/locales/ru.js | 5 +- .../src/app/Notifications/Notifications.tsx | 4 +- .../NotificationItemController.ts | 2 +- .../NotificationsItem/NotificationsItem.tsx | 12 +- .../core/src/eventsLog/INotification.ts | 2 +- .../core/src/eventsLog/NotificationService.ts | 28 +-- ...skStatus.gql => asyncExportTaskStatus.gql} | 4 +- .../packages/core/src/root/SessionService.ts | 2 +- .../core/src/root/queries/openSession.gql | 1 - .../core/src/root/queries/sessionState.gql | 1 - webapp/packages/core/src/sdk/sdk.ts | 18 +- .../core/src/utils/Promises/Deferred.ts | 7 +- .../src/DataExportMenuService.ts | 1 - .../src/DataExportProcessService.ts | 129 ++++++++++++++ .../src/DataExportService.ts | 106 ++---------- .../src/Dialog/DataExportController.ts | 31 +--- .../src/Dialog/DataExportDialog.tsx | 2 +- .../src/Dialog/ProcessorSelectDialog.tsx | 12 +- .../src/ExportFromContainerProcess.ts | 14 +- .../src/ExportFromResultsProcess.ts | 14 +- .../src/ExportNotification/Controller.ts | 110 ++++++++++++ .../ExportNotification/ExportNotification.tsx | 126 ++++++++++++++ .../src/PendingNotification.tsx | 163 ------------------ .../data-export-plugin/src/manifest.ts | 4 +- .../TableFooterMenu/TableFooterMenuService.ts | 4 +- 26 files changed, 475 insertions(+), 332 deletions(-) rename webapp/packages/core/src/queries/DataTransfer/{asyncTaskStatus.gql => asyncExportTaskStatus.gql} (90%) create mode 100644 webapp/packages/data-export-plugin/src/DataExportProcessService.ts create mode 100644 webapp/packages/data-export-plugin/src/ExportNotification/Controller.ts create mode 100644 webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx delete mode 100644 webapp/packages/data-export-plugin/src/PendingNotification.tsx diff --git a/webapp/packages/core/assets/locales/en.js b/webapp/packages/core/assets/locales/en.js index 7544839116..d149ede0e6 100644 --- a/webapp/packages/core/assets/locales/en.js +++ b/webapp/packages/core/assets/locales/en.js @@ -30,12 +30,15 @@ module.exports = { data_transfer_dialog_title: 'Export data', 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', + data_transfer_notification_download: 'Download', + data_transfer_notification_delete: 'Delete', + data_transfer_exporting_table: 'Table:', + data_transfer_exporting_sql: 'SQL:', ui_stepper_back: 'Back', ui_processing_loading: 'Loading...', diff --git a/webapp/packages/core/assets/locales/ru.js b/webapp/packages/core/assets/locales/ru.js index 5c32d5cdbc..32b530a9aa 100644 --- a/webapp/packages/core/assets/locales/ru.js +++ b/webapp/packages/core/assets/locales/ru.js @@ -29,12 +29,15 @@ module.exports = { data_transfer_dialog_title: 'Экспорт данных', 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: 'Подготовка файла к загрузке не удалась или была отменена', + data_transfer_notification_download: 'Загрузить', + data_transfer_notification_delete: 'Удалить', + data_transfer_exporting_table: 'Таблица:', + data_transfer_exporting_sql: 'SQL:', ui_stepper_back: 'Назад', ui_processing_loading: 'Загрузка...', diff --git a/webapp/packages/core/src/app/Notifications/Notifications.tsx b/webapp/packages/core/src/app/Notifications/Notifications.tsx index d3cf1ce822..7215a9e257 100644 --- a/webapp/packages/core/src/app/Notifications/Notifications.tsx +++ b/webapp/packages/core/src/app/Notifications/Notifications.tsx @@ -32,8 +32,8 @@ export const Notifications = observer(function Notifications() { return styled(useStyles(styles))( - {notificationService.notifications.map(notification => ( - + {notificationService.notificationList.keys.map(notificationId => ( + ))} diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts index 7cb58aa34b..3b1a408da0 100644 --- a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts @@ -31,7 +31,7 @@ export class NotificationItemController implements IInitializableController { this.handleShowDetails = this.showDetails.bind(this); } - if (notification.type !== ENotificationType.Error && !notification.persistent) { + if (notification.type !== ENotificationType.Error) { this.closeAfter = FadeTimeout; } } diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx index 85169e5aa0..b94d940f67 100644 --- a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx @@ -8,18 +8,20 @@ import { observer } from 'mobx-react'; -import { useController } from '@dbeaver/core/di'; -import { INotification } from '@dbeaver/core/eventsLog'; +import { useController, useService } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; import { NotificationItemController } from './NotificationItemController'; import { Snackbar } from './Snackbar/Snackbar'; type NotificationProps = { - notification: INotification; + notificationId: number; } -export const NotificationsItem = observer(function Notification({ notification }: NotificationProps) { - if (!notification) { +export const NotificationsItem = observer(function Notification({ notificationId }: NotificationProps) { + const notificationService = useService(NotificationService); + const notification = notificationService.notificationList.get(notificationId); + if (!notification || notification.isSilent) { return null; } const controller = useController(NotificationItemController, notification); diff --git a/webapp/packages/core/src/eventsLog/INotification.ts b/webapp/packages/core/src/eventsLog/INotification.ts index c51075edc1..6ea55579a2 100644 --- a/webapp/packages/core/src/eventsLog/INotification.ts +++ b/webapp/packages/core/src/eventsLog/INotification.ts @@ -9,6 +9,7 @@ export enum ENotificationType { Info = 'Info', Error = 'Error', + Custom =' Custom' } export type NotificationComponentProps = { @@ -23,7 +24,6 @@ export interface INotification { title: string; message?: string; details?: string | Error; - persistent: boolean; isSilent: boolean; customComponent?: () => NotificationComponent; source: T; diff --git a/webapp/packages/core/src/eventsLog/NotificationService.ts b/webapp/packages/core/src/eventsLog/NotificationService.ts index 7b1ec0dac6..199bf3cba0 100644 --- a/webapp/packages/core/src/eventsLog/NotificationService.ts +++ b/webapp/packages/core/src/eventsLog/NotificationService.ts @@ -12,21 +12,15 @@ import { injectable } from '@dbeaver/core/di'; import { GQLError, ServerInternalError } from '@dbeaver/core/sdk'; import { EntityList } from '@dbeaver/core/utils'; -import { ENotificationType, INotification, INotificationOptions } from './INotification'; +import { + ENotificationType, INotification, INotificationOptions, NotificationComponent +} from './INotification'; @injectable() export class NotificationService { - private notificationList = new EntityList>(({ id }) => id); + readonly notificationList = new EntityList>(({ id }) => id); private notificationNextId = 0 - /** - * By default filtered without silent notifications - */ - @computed get notifications(): INotification[] { - return this.notificationList.values - .filter(notification => !notification.isSilent); - } - notify(options: INotificationOptions, type: ENotificationType) { const id = this.notificationNextId++; @@ -36,7 +30,6 @@ export class NotificationService { message: options.message, details: options.details, isSilent: !!options.isSilent, - persistent: !!options.persistent, customComponent: options.customComponent, source: options.source!, type, @@ -46,6 +39,19 @@ export class NotificationService { this.notificationList.set(notification); } + customNotification( + component: () => NotificationComponent, + source?: T, + options?: INotificationOptions + ) { + this.notify({ + title: '', + ...options, + customComponent: component, + source, + }, ENotificationType.Custom); + } + logInfo(notification: INotificationOptions) { this.notify(notification, ENotificationType.Info); } diff --git a/webapp/packages/core/src/queries/DataTransfer/asyncTaskStatus.gql b/webapp/packages/core/src/queries/DataTransfer/asyncExportTaskStatus.gql similarity index 90% rename from webapp/packages/core/src/queries/DataTransfer/asyncTaskStatus.gql rename to webapp/packages/core/src/queries/DataTransfer/asyncExportTaskStatus.gql index d835660f77..35f3e38ce3 100644 --- a/webapp/packages/core/src/queries/DataTransfer/asyncTaskStatus.gql +++ b/webapp/packages/core/src/queries/DataTransfer/asyncExportTaskStatus.gql @@ -5,12 +5,14 @@ mutation asyncExportTaskStatus( id: $taskId ) { id + name running - taskResult + status error { message errorCode stackTrace } + taskResult } } diff --git a/webapp/packages/core/src/root/SessionService.ts b/webapp/packages/core/src/root/SessionService.ts index 2084f6baba..e252271dd9 100644 --- a/webapp/packages/core/src/root/SessionService.ts +++ b/webapp/packages/core/src/root/SessionService.ts @@ -13,7 +13,7 @@ import { import { SessionSettingsService } from './SessionSettingsService'; -export type SessionState = Pick & { +export type SessionState = Pick & { connections: Array>; }; diff --git a/webapp/packages/core/src/root/queries/openSession.gql b/webapp/packages/core/src/root/queries/openSession.gql index 2aca32e58c..9beeda52e2 100644 --- a/webapp/packages/core/src/root/queries/openSession.gql +++ b/webapp/packages/core/src/root/queries/openSession.gql @@ -1,6 +1,5 @@ mutation openSession { session: openSession { - id createTime lastAccessTime cacheExpired diff --git a/webapp/packages/core/src/root/queries/sessionState.gql b/webapp/packages/core/src/root/queries/sessionState.gql index 1029b95c13..f654c04cf0 100644 --- a/webapp/packages/core/src/root/queries/sessionState.gql +++ b/webapp/packages/core/src/root/queries/sessionState.gql @@ -1,6 +1,5 @@ query sessionState { sessionState { - id createTime lastAccessTime locale diff --git a/webapp/packages/core/src/sdk/sdk.ts b/webapp/packages/core/src/sdk/sdk.ts index aa5d677d98..64259a5b1b 100644 --- a/webapp/packages/core/src/sdk/sdk.ts +++ b/webapp/packages/core/src/sdk/sdk.ts @@ -472,6 +472,8 @@ export type QueryMetadataGetNodeDdlArgs = { export type ServerConfig = { name: Scalars["String"]; version: Scalars["String"]; + anonymousAccessEnabled?: Maybe; + authenticationEnabled?: Maybe; supportsPredefinedConnections?: Maybe; supportsProvidedConnections?: Maybe; supportsCustomConnections?: Maybe; @@ -501,7 +503,6 @@ export type ServerMessage = { }; export type SessionInfo = { - id: Scalars["ID"]; createTime: Scalars["String"]; lastAccessTime: Scalars["String"]; locale: Scalars["String"]; @@ -859,7 +860,10 @@ export type AsyncExportTaskStatusMutationVariables = { }; export type AsyncExportTaskStatusMutation = { - taskInfo: Pick & { + taskInfo: Pick< + AsyncTaskInfo, + "id" | "name" | "running" | "status" | "taskResult" + > & { error: Maybe>; }; }; @@ -1251,7 +1255,7 @@ export type OpenSessionMutation = { session: Maybe< Pick< SessionInfo, - "id" | "createTime" | "lastAccessTime" | "cacheExpired" | "locale" + "createTime" | "lastAccessTime" | "cacheExpired" | "locale" > & { connections: Array< Maybe> @@ -1288,7 +1292,7 @@ export type SessionStateQuery = { sessionState: Maybe< Pick< SessionInfo, - "id" | "createTime" | "lastAccessTime" | "locale" | "cacheExpired" + "createTime" | "lastAccessTime" | "locale" | "cacheExpired" > & { connections: Array< Maybe> @@ -1507,13 +1511,15 @@ export const AsyncExportTaskStatusDocument = gql` mutation asyncExportTaskStatus($taskId: String!) { taskInfo: asyncTaskStatus(id: $taskId) { id + name running - taskResult + status error { message errorCode stackTrace } + taskResult } } `; @@ -1890,7 +1896,6 @@ export const UpdateResultsDataDocument = gql` export const OpenSessionDocument = gql` mutation openSession { session: openSession { - id createTime lastAccessTime cacheExpired @@ -1927,7 +1932,6 @@ export const ServerConfigDocument = gql` export const SessionStateDocument = gql` query sessionState { sessionState { - id createTime lastAccessTime locale diff --git a/webapp/packages/core/src/utils/Promises/Deferred.ts b/webapp/packages/core/src/utils/Promises/Deferred.ts index d2beb4a2cb..f864aa3254 100644 --- a/webapp/packages/core/src/utils/Promises/Deferred.ts +++ b/webapp/packages/core/src/utils/Promises/Deferred.ts @@ -74,9 +74,10 @@ export class Deferred { } @action - protected toCancelled(): void { + protected toCancelled(reason?: any): void { this.state = EDeferredState.CANCELLED; - this.promiseExecutor.reject(new PromiseCancelledError()); + this.rejectionReason = reason; + this.promiseExecutor.reject(reason || new PromiseCancelledError()); } @action @@ -101,7 +102,7 @@ export class DeferredFromPromise extends Deferred { value => this.toResolved(value), (err) => { if (err instanceof PromiseCancelledError) { - this.toCancelled(); + this.toCancelled(err); } else { this.toRejected(err); } diff --git a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts index f6469d864f..e975c7cfd5 100644 --- a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts +++ b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts @@ -30,7 +30,6 @@ export class DataExportMenuService { title: 'Export', icon: 'export', onClick: this.exportData.bind(this), - // isPanel: true, }; this.tableFooterMenuService.registerMenuItem(exportData); } diff --git a/webapp/packages/data-export-plugin/src/DataExportProcessService.ts b/webapp/packages/data-export-plugin/src/DataExportProcessService.ts new file mode 100644 index 0000000000..39e93099bd --- /dev/null +++ b/webapp/packages/data-export-plugin/src/DataExportProcessService.ts @@ -0,0 +1,129 @@ +/* + * 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 { injectable } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; +import { GraphQLService, DataTransferParameters } from '@dbeaver/core/sdk'; +import { Deferred, EntityList } from '@dbeaver/core/utils'; + +import { ExportFromContainerProcess } from './ExportFromContainerProcess'; +import { ExportFromResultsProcess } from './ExportFromResultsProcess'; +import { IExportContext } from './IExportContext'; + +type Process = { + taskId: string; + process: Deferred; +} + +type ExportProcess = { + taskId: string; + context: IExportContext; + parameters: DataTransferParameters; + process: Deferred; +} + +@injectable() +export class DataExportProcessService { + readonly exportProcesses = new EntityList(value => value.taskId); + + constructor( + private graphQLService: GraphQLService, + private notificationService: NotificationService, + ) { } + + async cancel(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + process.process.cancel(); + } + + async delete(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + try { + const dataFileId = process.process.getPayload(); + if (dataFileId) { + await this.graphQLService.gql.removeDataTransferFile({ dataFileId }); + } + } catch (exception) { + this.notificationService.logException(exception, 'Error occurred while deleting file'); + } finally { + this.exportProcesses.remove(exportId); + } + } + + download(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + const dataFileId = process.process.getPayload(); + if (!dataFileId) { + return; + } + this.exportProcesses.remove(exportId); + } + + downloadUrl(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + const dataFileId = process.process.getPayload(); + if (!dataFileId) { + return; + } + return `/dbeaver/data/${dataFileId}`; + } + + async exportData( + context: IExportContext, + parameters: DataTransferParameters + ): Promise { + let process: Process; + if (context.containerNodePath) { + process = await this.exportFromContainer(context.connectionId, context.containerNodePath, parameters); + } else { + process = await this.exportFromResults(context.connectionId, context.contextId!, context.resultId!, parameters); + } + + this.exportProcesses.set({ + ...process, + context, + parameters, + }); + + return process.taskId; + } + + private async exportFromContainer( + connectionId: string, + containerNodePath: string, + parameters: DataTransferParameters + ): Promise { + const process = new ExportFromContainerProcess(this.graphQLService, this.notificationService); + const taskId = await process.start(connectionId, containerNodePath, parameters); + return { taskId, process }; + } + + private async exportFromResults( + connectionId: string, + contextId: string, + resultsId: string, + parameters: DataTransferParameters + ): Promise { + const process = new ExportFromResultsProcess(this.graphQLService, this.notificationService); + const taskId = await process.start(connectionId, contextId, resultsId, parameters); + + return { taskId, process }; + } +} diff --git a/webapp/packages/data-export-plugin/src/DataExportService.ts b/webapp/packages/data-export-plugin/src/DataExportService.ts index 5458effd7a..e90b11ca1f 100644 --- a/webapp/packages/data-export-plugin/src/DataExportService.ts +++ b/webapp/packages/data-export-plugin/src/DataExportService.ts @@ -6,18 +6,15 @@ * 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 } from '@dbeaver/core/sdk'; -import { Deferred } from '@dbeaver/core/utils'; -import { ExportFromContainerProcess } from './ExportFromContainerProcess'; -import { ExportFromResultsProcess } from './ExportFromResultsProcess'; -import { PendingNotification } from './PendingNotification'; +import { DataExportProcessService } from './DataExportProcessService'; +import { ExportNotification } from './ExportNotification/ExportNotification'; +import { IExportContext } from './IExportContext'; type ProcessorsResourceMetadata = { loaded: boolean; @@ -30,109 +27,40 @@ export class DataExportService { this.refreshProcessorsAsync.bind(this), (_, { loaded }) => loaded ); - readonly exportProcesses = new Map>(); constructor( private graphQLService: GraphQLService, private notificationService: NotificationService, - private commonDialogService: CommonDialogService, + private dataExportProcessService: DataExportProcessService, ) { } async cancel(exportId: string) { - const process = this.exportProcesses.get(exportId); - if (!process) { - return; - } - process.cancel(); + await this.dataExportProcessService.cancel(exportId); } async delete(exportId: string) { - const process = this.exportProcesses.get(exportId); - if (!process) { - return; - } - const dataFileId = process.getPayload(); - if (!dataFileId) { - return; - } - try { - await this.graphQLService.gql.removeDataTransferFile({ dataFileId }); - this.exportProcesses.delete(exportId); - } 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 { - } + await this.dataExportProcessService.delete(exportId); } download(exportId: string) { - const process = this.exportProcesses.get(exportId); - if (!process) { - return; - } - const dataFileId = process.getPayload(); - if (!dataFileId) { - return; - } - this.exportProcesses.delete(exportId); + this.dataExportProcessService.download(exportId); } downloadUrl(exportId: string) { - const process = this.exportProcesses.get(exportId); - if (!process) { - return; - } - const dataFileId = process.getPayload(); - if (!dataFileId) { - return; - } - return `/dbeaver/data/${dataFileId}`; + return this.dataExportProcessService.download(exportId); } - async exportFromContainer( - connectionId: string, - containerNodePath: string, + async exportData( + context: IExportContext, parameters: DataTransferParameters - ): Promise { - const process = new ExportFromContainerProcess(this.graphQLService, this.notificationService); - const taskId = await process.start(connectionId, containerNodePath, parameters); - if (taskId) { - this.exportProcesses.set(taskId, process); - this.showPendingNotification(taskId); - return taskId; - } - } + ) { + const taskId = await this.dataExportProcessService.exportData( + context, + parameters + ); - async exportFromResults( - connectionId: string, - contextId: string, - resultsId: string, - parameters: DataTransferParameters - ): Promise { - const process = new ExportFromResultsProcess(this.graphQLService, this.notificationService); - const taskId = await process.start(connectionId, contextId, resultsId, parameters); - if (taskId) { - this.exportProcesses.set(taskId, process); - 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, - }); + this.notificationService.customNotification(() => ExportNotification, taskId); + return taskId; } private async refreshProcessorsAsync( diff --git a/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts b/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts index 8c0452e4cd..9c3479c46d 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts +++ b/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts @@ -55,33 +55,20 @@ export class DataExportController implements IInitializableController { this.loadProcessors(); } - export = async () => { + prepareExport = async () => { if (!this.processor || this.isExporting) { return; } this.isExporting = true; try { - if (this.context.containerNodePath) { - await this.dataExportService.exportFromContainer( - this.context.connectionId, - this.context.containerNodePath, - { - processorId: this.processor.id, - processorProperties: this.processorProperties, - } - ); - } else if (this.context.contextId && this.context.resultId) { - await this.dataExportService.exportFromResults( - this.context.connectionId, - this.context.contextId, - this.context.resultId, - { - processorId: this.processor.id, - processorProperties: this.processorProperties, - } - ); - } + await this.dataExportService.exportData( + this.context, + { + processorId: this.processor.id, + processorProperties: this.processorProperties, + } + ); this.close(); } catch (exception) { this.notificationService.logException(exception, 'Can\'t export'); @@ -128,6 +115,6 @@ export class DataExportController implements IInitializableController { return (processorA.name || '').localeCompare((processorB.name || '')); } - return (processorB.order || 0) - (processorA.order || 0); + return processorA.order - processorB.order; } } diff --git a/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx index 61d7634569..dbb0db6e68 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx @@ -29,7 +29,7 @@ export const DataExportDialog: DialogComponent = observer( isExporting={controller.isExporting} onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)} onClose={props.rejectDialog} - onExport={controller.export} + onExport={controller.prepareExport} /> ); } diff --git a/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx index 3410771dec..9004a37226 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx @@ -29,7 +29,14 @@ const styles = css` } export-object { composes: theme-typography--body2 from global; + flex-shrink: 0; padding: 16px; + max-height: 50px; + overflow: hidden; + + & pre { + margin: 0; + } } `; @@ -58,7 +65,10 @@ export const ProcessorSelectDialog = observer( noBodyPadding onReject={onClose} > - {translate('data_transfer_dialog_exporting_object')} {context.sourceName || node?.name} + + {context.sourceName ? translate('data_transfer_exporting_sql') : `${translate('data_transfer_exporting_table')} ${node?.name}`} +
{context.sourceName}
+
{isLoading && } {!isLoading && } diff --git a/webapp/packages/data-export-plugin/src/ExportFromContainerProcess.ts b/webapp/packages/data-export-plugin/src/ExportFromContainerProcess.ts index 1913b87489..419cee02c4 100644 --- a/webapp/packages/data-export-plugin/src/ExportFromContainerProcess.ts +++ b/webapp/packages/data-export-plugin/src/ExportFromContainerProcess.ts @@ -32,7 +32,7 @@ export class ExportFromContainerProcess extends Deferred { connectionId: string, containerNodePath: string, parameters: DataTransferParameters - ): Promise { + ): Promise { // start async task try { const { taskInfo } = await this.graphQLService.gql.exportDataFromContainer({ @@ -45,14 +45,14 @@ export class ExportFromContainerProcess extends Deferred { if (this.getState() === EDeferredState.CANCELLING) { await this.cancelAsync(this.taskId); } + + this.statusUpdateProcess(); + + return this.taskId; } catch (e) { this.onError(e); throw e; } - - this.statusUpdateProcess(); - - return this.taskId; } /** @@ -133,9 +133,7 @@ export class ExportFromContainerProcess extends Deferred { private onError(error: Error, status?: string) { // if task failed to execute during cancelling - it means it was cancelled successfully if (this.getState() === EDeferredState.CANCELLING) { - this.toCancelled(); - const message = `Data export has been canceled${status ? `: ${status}` : ''}`; - this.notificationService.logException(error, message); + this.toCancelled(error); } else { this.toRejected(error); } diff --git a/webapp/packages/data-export-plugin/src/ExportFromResultsProcess.ts b/webapp/packages/data-export-plugin/src/ExportFromResultsProcess.ts index d8b184b8d0..dc99ace903 100644 --- a/webapp/packages/data-export-plugin/src/ExportFromResultsProcess.ts +++ b/webapp/packages/data-export-plugin/src/ExportFromResultsProcess.ts @@ -33,7 +33,7 @@ export class ExportFromResultsProcess extends Deferred { contextId: string, resultsId: string, parameters: DataTransferParameters - ): Promise { + ): Promise { // start async task try { const { taskInfo } = await this.graphQLService.gql.exportDataFromResults({ @@ -47,14 +47,14 @@ export class ExportFromResultsProcess extends Deferred { if (this.getState() === EDeferredState.CANCELLING) { await this.cancelAsync(this.taskId); } + + this.statusUpdateProcess(); + + return this.taskId; } catch (e) { this.onError(e); throw e; } - - this.statusUpdateProcess(); - - return this.taskId; } /** @@ -135,9 +135,7 @@ export class ExportFromResultsProcess extends Deferred { private onError(error: Error, status?: string) { // if task failed to execute during cancelling - it means it was cancelled successfully if (this.getState() === EDeferredState.CANCELLING) { - this.toCancelled(); - const message = `Data export has been canceled${status ? `: ${status}` : ''}`; - this.notificationService.logException(error, message); + this.toCancelled(error); } else { this.toRejected(error); } diff --git a/webapp/packages/data-export-plugin/src/ExportNotification/Controller.ts b/webapp/packages/data-export-plugin/src/ExportNotification/Controller.ts new file mode 100644 index 0000000000..cf86665d38 --- /dev/null +++ b/webapp/packages/data-export-plugin/src/ExportNotification/Controller.ts @@ -0,0 +1,110 @@ +/* + * 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 { observable, computed } from 'mobx'; + +import { ErrorDetailsDialog, NodesManagerService } from '@dbeaver/core/app'; +import { IInitializableController, injectable } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; +import { INotification } from '@dbeaver/core/eventsLog'; +import { LocalizationService } from '@dbeaver/core/localization'; +import { EDeferredState } from '@dbeaver/core/utils'; + +import { DataExportProcessService } from '../DataExportProcessService'; + +@injectable() +export class Controller implements IInitializableController { + @observable isDetailsDialogOpen = false; + + get isSuccess() { + return this.process?.getState() === EDeferredState.RESOLVED; + } + + get isPending() { + return this.process?.isInProgress; + } + + get process() { + return this.task?.process; + } + + get task() { + return this.dataExportProcessService.exportProcesses.get(this.notification.source); + } + + get hasDetails() { + return !!this.process?.getRejectionReason(); + } + + @computed get sourceName() { + if (!this.task) { + return ''; + } + if (this.task.context.containerNodePath) { + const node = this.nodesManagerService.getNode(this.task.context.containerNodePath); + + return `${this.localization.translate('data_transfer_exporting_table')} ${node?.name}`; + } + + return this.localization.translate('data_transfer_exporting_sql'); + } + + get status() { + switch (this.process?.getState()) { + case EDeferredState.PENDING: + return 'data_transfer_notification_preparation'; + case EDeferredState.CANCELLING: + return 'ui_processing_canceling'; + case EDeferredState.RESOLVED: + return 'data_transfer_notification_ready'; + default: + return 'data_transfer_notification_error'; + } + } + + get downloadUrl() { + return this.dataExportProcessService.downloadUrl(this.notification.source); + } + + private notification!: INotification; + + constructor( + private commonDialogService: CommonDialogService, + private dataExportProcessService: DataExportProcessService, + private nodesManagerService: NodesManagerService, + private localization: LocalizationService, + ) {} + + init(notification: INotification) { + this.notification = notification; + } + + delete = () => { + this.dataExportProcessService.delete(this.notification.source); + this.notification.close(); + } + + download = () => { + this.dataExportProcessService.download(this.notification.source); + this.notification.close(); + } + + cancel = () => { + this.dataExportProcessService.cancel(this.notification.source); + } + + showDetails= async () => { + this.isDetailsDialogOpen = true; + try { + this.notification.showDetails(); + await this.commonDialogService.open(ErrorDetailsDialog, this.process?.getRejectionReason()); + } finally { + this.isDetailsDialogOpen = false; + } + } +} diff --git a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx new file mode 100644 index 0000000000..b57a8f21d5 --- /dev/null +++ b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx @@ -0,0 +1,126 @@ +/* + * 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 } 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 { useController } 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 { Controller } from './Controller'; + +const styles = css` + Loader { + margin-right: 16px; + } + actions { + display: flex; + } + fill { + flex: 1; + } + Button { + margin-left: 16px !important; + } + actions Button { + display: flex; + } + source-name { + composes: theme-typography--body2 from global; + padding-top: 16px; + max-height: 50px; + overflow: hidden; + + & pre { + margin: 0; + } + } +`; + +export const ExportNotification = observer(function ExportNotification({ + notification, +}: NotificationComponentProps) { + const controller = useController(Controller, notification); + const translate = useTranslate(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return styled(useStyles(SNACKBAR_STYLES, styles))( + + + + {translate(controller.status)} + {!controller.isPending && ( + + + + )} + + + + {controller.sourceName} +
{controller.task?.context.sourceName}
+
+ + + {controller.isSuccess && ( + <> + + + + )} + {controller.hasDetails && ( + + )} + {controller.isPending && ( + + )} + +
+
+ ); +}); diff --git a/webapp/packages/data-export-plugin/src/PendingNotification.tsx b/webapp/packages/data-export-plugin/src/PendingNotification.tsx deleted file mode 100644 index 941a137f5b..0000000000 --- a/webapp/packages/data-export-plugin/src/PendingNotification.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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) { - 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))( - - - - {status()} - {task.isFinished && ( - - - - )} - - - - - {task.isFinished && isSuccess && ( - <> - - - - )} - {task.isFinished && !isSuccess && task.getRejectionReason() && ( - - )} - {!task.isFinished && ( - - )} - - - - ); -}); diff --git a/webapp/packages/data-export-plugin/src/manifest.ts b/webapp/packages/data-export-plugin/src/manifest.ts index a0d3787569..d0d262d523 100644 --- a/webapp/packages/data-export-plugin/src/manifest.ts +++ b/webapp/packages/data-export-plugin/src/manifest.ts @@ -10,16 +10,18 @@ import { IServiceInjector, PluginManifest } from '@dbeaver/core/di'; import { Bootstrap } from './Bootstrap'; import { DataExportMenuService } from './DataExportMenuService'; +import { DataExportProcessService } from './DataExportProcessService'; import { DataExportService } from './DataExportService'; export const manifest: PluginManifest = { info: { - name: 'Data Viewer Plugin', + name: 'Data Export Plugin', }, providers: [ DataExportMenuService, DataExportService, + DataExportProcessService, ], async initialize(services: IServiceInjector) { diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index 24d9187c3f..bfba2d1df2 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -33,7 +33,7 @@ export class TableFooterMenuService { return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems; } - registerMenuItem(options: IContextMenuItem, panelId?: string): void { - this.contextMenuService.addMenuItem(panelId || this.tableFooterMenuToken, options); + registerMenuItem(options: IContextMenuItem): void { + this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); } } From 162269c165798edbc4d37925fc17d8990b19c911 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 6 May 2020 17:30:46 +0300 Subject: [PATCH 2/9] refactor(core): navigation tabs api cleanup --- .../src/app/shared/NavigationTabs/ITab.ts | 2 -- .../NavigationTabs/NavigationTabsService.ts | 6 ++---- .../app/shared/NavigationTabs/TabHandler.ts | 4 ---- .../src/TableViewer/TableViewerModel.ts | 2 +- .../src/IObjectViewerTabState.ts | 2 ++ .../src/ObjectViewerTab.tsx | 4 ++-- .../src/ObjectViewerTabService.ts | 21 +++++++++++-------- .../sql-editor/src/SqlEditorTabService.ts | 2 -- 8 files changed, 19 insertions(+), 24 deletions(-) diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts index 4f223a430b..d336f9f112 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts @@ -10,8 +10,6 @@ export interface ITabOptions { id?: string; handlerId: string; handlerState: T; - name?: string; - icon?: string; } export interface ITab extends ITabOptions { diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts index bc4f981b91..de9259fccc 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts @@ -47,8 +47,8 @@ export class NavigationTabsService { return this.state.tabs; } - onTabSelect = new Subject(); - onTabClose = new Subject(); + readonly onTabSelect = new Subject(); + readonly onTabClose = new Subject(); constructor( private notificationService: NotificationService, @@ -64,8 +64,6 @@ export class NavigationTabsService { if ( typeof value.id === 'string' && typeof value.handlerId === 'string' - && (!value.name || typeof value.name === 'string') - && (!value.icon || typeof value.icon === 'string') ) { map[key] = value; } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts b/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts index b3942eeac8..6a59db0a9a 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts @@ -31,8 +31,6 @@ export type TabRestoreEvent = (tab: ITab) => Promise | bool export type TabHandlerOptions = { key: string; - navigatorId: string; - priority: number; getTabComponent(): TabHandlerTabComponent; getPanelComponent(): TabHandlerPanelComponent; onSelect?: TabHandlerEvent; @@ -43,7 +41,6 @@ export type TabHandlerOptions = { export class TabHandler { key: string - navigatorId: string getTabComponent: () => TabHandlerTabComponent; getPanelComponent: () => TabHandlerPanelComponent; onSelect?: TabHandlerEvent @@ -53,7 +50,6 @@ export class TabHandler { constructor(options: TabHandlerOptions) { this.key = options.key; - this.navigatorId = options.navigatorId; this.getTabComponent = options.getTabComponent; this.getPanelComponent = options.getPanelComponent; this.onSelect = options.onSelect; diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts index 47b6e1bcdf..47f312c075 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts @@ -34,7 +34,7 @@ export interface ITableViewerModelOptions { containerNodePath?: string; resultId?: string | null; // will be filled after fist data fetch executionContext?: IExecutionContext | null; // will be filled before fist data fetch - sourceName?: string; + sourceName?: string; // TODO: refactor it, used for showing sql query for export initialState?: IRequestDataResult; noLoaderWhileRequestingDataAsync?: boolean; requestDataAsync(model: TableViewerModel, rowOffset: number, count: number): Promise; diff --git a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts index 7e07f2a27c..df6d46a8a2 100644 --- a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts +++ b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts @@ -9,6 +9,8 @@ export interface IObjectViewerTabState { objectId: string; folderId: string; + tabIcon?: string; + tabTitle?: string; pageId: string; pagesState: Map; } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx index df804af32f..4e4723b91c 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx @@ -22,8 +22,8 @@ export const ObjectViewerTab = observer(function ObjectViewerTab({ return styled(useStyles(...style))( - - + + ); }); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts index 463492b970..5a724f009c 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts @@ -46,8 +46,6 @@ export class ObjectViewerTabService { this.tabHandler = this.navigationTabsService .registerTabHandler({ key: objectViewerTabHandlerKey, - navigatorId: 'database', - priority: 1, getTabComponent: () => ObjectViewerTab, getPanelComponent: () => ObjectViewerPanel, onRestore: this.restoreObjectTab.bind(this), @@ -132,11 +130,16 @@ export class ObjectViewerTabService { } async restoreObjectTab(tab: ITab) { - if (typeof tab.handlerState?.folderId === 'string' && typeof tab.handlerState?.objectId === 'string') { + if ( + typeof tab.handlerState?.folderId === 'string' + && typeof tab.handlerState?.objectId === 'string' + && (!tab.handlerState.tabIcon || typeof tab.handlerState.tabIcon === 'string') + && (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string') + ) { const node = await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId); if (node) { - tab.icon = node.icon; - tab.name = node.name; + tab.handlerState.tabIcon = node.icon; + tab.handlerState.tabTitle = node.name; return this.dbObjectPageService.restorePages(tab); } @@ -162,8 +165,8 @@ export class ObjectViewerTabService { ); if (tab) { - tab.name = nodeInfo.name; - tab.icon = nodeInfo.icon; + tab.handlerState.tabIcon = nodeInfo.icon; + tab.handlerState.tabTitle = nodeInfo.name; tabInfo.registerTab(tab); } else { tabInfo.openNewTab({ @@ -173,9 +176,9 @@ export class ObjectViewerTabService { folderId: nodeInfo.folderId, pageId: '', pagesState: new Map(), + tabIcon: nodeInfo.icon, + tabTitle: nodeInfo.name, }, - name: nodeInfo.name, - icon: nodeInfo.icon, }); } } diff --git a/webapp/packages/sql-editor/src/SqlEditorTabService.ts b/webapp/packages/sql-editor/src/SqlEditorTabService.ts index f06b61c964..5078915cd8 100644 --- a/webapp/packages/sql-editor/src/SqlEditorTabService.ts +++ b/webapp/packages/sql-editor/src/SqlEditorTabService.ts @@ -43,8 +43,6 @@ export class SqlEditorTabService { this.tabHandler = this.navigationTabsService .registerTabHandler({ key: sqlEditorTabHandlerKey, - navigatorId: 'sql-editor', - priority: 1, getTabComponent: () => SqlEditorTab, getPanelComponent: () => SqlEditorPanel, onRestore: this.handleTabRestore.bind(this), From e970eb3cfd990b2fc022d8848eb812379ad491d4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 7 May 2020 16:41:25 +0300 Subject: [PATCH 3/9] feat(core): implement IconButton block --- .../NotificationsItem/Snackbar/Snackbar.tsx | 7 ++-- .../NotificationsItem/Snackbar/styles.ts | 24 ++++--------- .../packages/core/src/blocks/IconButton.tsx | 35 +++++++++++++++++++ webapp/packages/core/src/blocks/index.ts | 1 + .../ExportNotification/ExportNotification.tsx | 20 ++--------- 5 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 webapp/packages/core/src/blocks/IconButton.tsx diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx index d28d344d09..4419f5b8ed 100644 --- a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx @@ -7,10 +7,9 @@ */ import { useEffect, useState } from 'react'; -import { Button as ReakitButton } from 'reakit/Button'; import styled, { use } from 'reshadow'; -import { Icon, Button } from '@dbeaver/core/blocks'; +import { Button, IconButton } from '@dbeaver/core/blocks'; import { ENotificationType } from '@dbeaver/core/eventsLog'; import { useTranslate } from '@dbeaver/core/localization'; import { useStyles } from '@dbeaver/core/theming'; @@ -66,9 +65,7 @@ export function Snackbar({ {type && } {text} {!closeAfter && onClose && ( - - - + )} diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts index 45f786dc87..17b97cd9ea 100644 --- a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts @@ -47,22 +47,6 @@ export const SNACKBAR_STYLES = composes( display: flex; } - ReakitButton { - color: rgba(0, 0, 0, 0.45); - outline: none; - border: none; - background: none; - cursor: pointer; - padding: 0; - margin: 4px; - height: 16px; - - & Icon { - width: 16px; - height: 16px; - } - } - message { flex: 1; overflow: auto; @@ -98,11 +82,15 @@ export const SNACKBAR_STYLES = composes( } actions { + display: flex; margin-top: 24px; & Button { - margin-left: auto; - display: block; + margin-left: 16px; + + &:first-child { + margin-left: auto; + } } } ` diff --git a/webapp/packages/core/src/blocks/IconButton.tsx b/webapp/packages/core/src/blocks/IconButton.tsx new file mode 100644 index 0000000000..b72ddb6e3a --- /dev/null +++ b/webapp/packages/core/src/blocks/IconButton.tsx @@ -0,0 +1,35 @@ +import { Button, ButtonProps } from 'reakit/Button'; +import styled, { css } from 'reshadow'; + +import { Icon } from './Icons/Icon'; + +const styles = css` + Button { + color: rgba(0, 0, 0, 0.45); + outline: none; + border: none; + background: none; + cursor: pointer; + padding: 0; + margin: 4px; + height: 16px; + + & Icon { + width: 16px; + height: 16px; + } + } +`; + +type Props = { + name: string; + viewBox: string; +} + +export function IconButton({ name, viewBox, ...rest }: Props & ButtonProps) { + return styled(styles)( + + ); +} diff --git a/webapp/packages/core/src/blocks/index.ts b/webapp/packages/core/src/blocks/index.ts index 00fd243969..c4d1f4138d 100644 --- a/webapp/packages/core/src/blocks/index.ts +++ b/webapp/packages/core/src/blocks/index.ts @@ -17,3 +17,4 @@ export * from './SubmittingForm'; export * from './SanitizedHTML/SanitizedHTML'; export * from './ItemList'; export * from './PropertiesTable'; +export * from './IconButton'; diff --git a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx index b57a8f21d5..ce6f0f0888 100644 --- a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx +++ b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx @@ -8,11 +8,10 @@ import { observer } from 'mobx-react'; import { useEffect, useState } 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 { Button, Loader, IconButton } from '@dbeaver/core/blocks'; import { useController } from '@dbeaver/core/di'; import { NotificationComponentProps } from '@dbeaver/core/eventsLog'; import { useTranslate } from '@dbeaver/core/localization'; @@ -25,18 +24,6 @@ const styles = css` Loader { margin-right: 16px; } - actions { - display: flex; - } - fill { - flex: 1; - } - Button { - margin-left: 16px !important; - } - actions Button { - display: flex; - } source-name { composes: theme-typography--body2 from global; padding-top: 16px; @@ -66,9 +53,7 @@ export const ExportNotification = observer(function ExportNotification({ {translate(controller.status)} {!controller.isPending && ( - - - + )} @@ -77,7 +62,6 @@ export const ExportNotification = observer(function ExportNotification({
{controller.task?.context.sourceName}
- {controller.isSuccess && ( <>