diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index ec0111a0cf..d6742cee8b 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -21,6 +21,7 @@ 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/SessionExpire/SessionExpireService'; export * from './NavigationTree/NavigationTreeService'; export * from './NavigationTree/NavigationTreeContextMenuService'; export * from './AppLocaleService'; diff --git a/webapp/packages/core-dialogs/src/SessionExpire/SessionExpireService.ts b/webapp/packages/core-app/src/shared/SessionExpire/SessionExpireService.ts similarity index 64% rename from webapp/packages/core-dialogs/src/SessionExpire/SessionExpireService.ts rename to webapp/packages/core-app/src/shared/SessionExpire/SessionExpireService.ts index 7a9096c371..c8dbec6d5c 100644 --- a/webapp/packages/core-dialogs/src/SessionExpire/SessionExpireService.ts +++ b/webapp/packages/core-app/src/shared/SessionExpire/SessionExpireService.ts @@ -8,12 +8,14 @@ import { Subject } from 'rxjs'; +import { ActionSnackbar } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; import { GQLError, GraphQLService, EServerErrorCode } from '@cloudbeaver/core-sdk'; -import { CommonDialogService } from '../CommonDialog/CommonDialogService'; import { SessionExpiredDialog } from './SessionExpiredDialog'; @injectable() @@ -23,11 +25,12 @@ export class SessionExpireService { onSessionExpire = new Subject(); constructor( private graphQLService: GraphQLService, + private notificationService: NotificationService, private commonDialogService: CommonDialogService ) { } - subscribe() { + subscribe(): void { this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); } @@ -40,7 +43,14 @@ export class SessionExpireService { && !this.isNotifiedAboutExpiredSession) { this.isNotifiedAboutExpiredSession = true; this.onSessionExpire.next(); - await this.commonDialogService.open(SessionExpiredDialog, null); + try { + await this.commonDialogService.open(SessionExpiredDialog, null); + } finally { + this.notificationService.customNotification(() => ActionSnackbar, { + actionText: 'app_root_session_expired_reload', + onAction: () => location.reload(), + }, { title: 'app_root_session_expired_title', persistent: true, type: ENotificationType.Error }); + } } throw exception; } diff --git a/webapp/packages/core-dialogs/src/SessionExpire/SessionExpiredDialog.tsx b/webapp/packages/core-app/src/shared/SessionExpire/SessionExpiredDialog.tsx similarity index 90% rename from webapp/packages/core-dialogs/src/SessionExpire/SessionExpiredDialog.tsx rename to webapp/packages/core-app/src/shared/SessionExpire/SessionExpiredDialog.tsx index dde252d8f6..924ca79494 100644 --- a/webapp/packages/core-dialogs/src/SessionExpire/SessionExpiredDialog.tsx +++ b/webapp/packages/core-app/src/shared/SessionExpire/SessionExpiredDialog.tsx @@ -11,12 +11,10 @@ import { useCallback } from 'react'; import styled, { css } from 'reshadow'; import { Button } from '@cloudbeaver/core-blocks'; +import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; -import { CommonDialogWrapper } from '../CommonDialog/CommonDialog/CommonDialogWrapper'; -import { DialogComponentProps } from '../CommonDialog/CommonDialogService'; - const styles = css` dialog-content { display: flex; diff --git a/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx new file mode 100644 index 0000000000..0448c30564 --- /dev/null +++ b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx @@ -0,0 +1,58 @@ +/* + * 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 { useEffect, useState } from 'react'; +import styled, { use } from 'reshadow'; + +import { Button, IconButton } from '@cloudbeaver/core-blocks'; +import { NotificationComponentProps } from '@cloudbeaver/core-events'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { NotificationMark } from '@cloudbeaver/core-notifications'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { SNACKBAR_COMMON_STYLES } from './SnackbarCommonStyles'; + +type ExtraProps = { + onAction: () => void; + actionText: string; +} + +export const ActionSnackbar: React.FC = function ActionSnackbar({ + notification, onAction, actionText, +}) { + const styles = useStyles(SNACKBAR_COMMON_STYLES); + const [mounted, setMounted] = useState(false); + const translate = useTranslate(); + + useEffect(() => { + setMounted(true); + }, []); + + return styled(styles)( + + + + {translate(notification.title)} + {!notification.persistent && ( + + )} + + + + + + + + ); +}; diff --git a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/styles.ts b/webapp/packages/core-blocks/src/Snackbars/SnackbarCommonStyles.ts similarity index 97% rename from webapp/packages/core-notifications/src/NotificationsItem/Snackbar/styles.ts rename to webapp/packages/core-blocks/src/Snackbars/SnackbarCommonStyles.ts index 1c1b2e0f5e..a23b7e70d7 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/styles.ts +++ b/webapp/packages/core-blocks/src/Snackbars/SnackbarCommonStyles.ts @@ -10,7 +10,7 @@ import { css } from 'reshadow'; import { composes } from '@cloudbeaver/core-theming'; -export const SNACKBAR_STYLES = composes( +export const SNACKBAR_COMMON_STYLES = composes( css` notification { composes: theme-background-surface theme-text-on-surface from global; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 0012699a46..d1f022baf7 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -33,3 +33,5 @@ export * from './TextPlaceholder'; export * from './useClipboard'; export * from './useFocus'; export * from './BlocksLocaleService'; +export * from './Snackbars/ActionSnackbar'; +export * from './Snackbars/SnackbarCommonStyles'; diff --git a/webapp/packages/core-bootstrap/src/AppBootstrap.ts b/webapp/packages/core-bootstrap/src/AppBootstrap.ts index 17546199db..c32fe07f54 100644 --- a/webapp/packages/core-bootstrap/src/AppBootstrap.ts +++ b/webapp/packages/core-bootstrap/src/AppBootstrap.ts @@ -11,10 +11,9 @@ import { NavigationTabsService, NavigationTreeContextMenuService, LogViewerMenuService, - ConnectionSchemaManagerService, + ConnectionSchemaManagerService, SessionExpireService } from '@cloudbeaver/core-app'; import { injectable } from '@cloudbeaver/core-di'; -import { SessionExpireService } from '@cloudbeaver/core-dialogs'; import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; import { ThemeService } from '@cloudbeaver/core-theming'; diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 2a4c4b5717..d357a37ec7 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -37,6 +37,7 @@ import { CoreSettingsService, AdministrationTopAppBarBootstrapService, AppLocaleService, + SessionExpireService } from '@cloudbeaver/core-app'; import { AuthInfoService, @@ -61,7 +62,7 @@ import { ConnectionsAdministrationNavService, } from '@cloudbeaver/core-connections'; import { PluginManifest } from '@cloudbeaver/core-di'; -import { CommonDialogService, ContextMenuService, SessionExpireService } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, ContextMenuService } from '@cloudbeaver/core-dialogs'; import { NotificationService, ExceptionsCatcherService, EventsSettingsService } from '@cloudbeaver/core-events'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { PluginManagerService } from '@cloudbeaver/core-plugin'; diff --git a/webapp/packages/core-dialogs/src/index.ts b/webapp/packages/core-dialogs/src/index.ts index 691d8ac6db..0952fa08e1 100644 --- a/webapp/packages/core-dialogs/src/index.ts +++ b/webapp/packages/core-dialogs/src/index.ts @@ -14,5 +14,3 @@ export * from './Menu/models/ComputedMenuPanelModel'; export * from './Menu/ContextMenu/ContextMenuService'; export * from './Menu/ContextMenu/IContextMenuItem'; export * from './Menu/ContextMenu/IMenuContext'; - -export * from './SessionExpire/SessionExpireService'; diff --git a/webapp/packages/core-events/src/EventsSettingsService.ts b/webapp/packages/core-events/src/EventsSettingsService.ts index c248ec4e62..c0a11b2f5b 100644 --- a/webapp/packages/core-events/src/EventsSettingsService.ts +++ b/webapp/packages/core-events/src/EventsSettingsService.ts @@ -11,6 +11,8 @@ import { PluginManagerService } from '@cloudbeaver/core-plugin'; const defaultSettings = { notificationsPool: 5, + maxPersistentAllow: 1, + }; export type EventsSettings = typeof defaultSettings diff --git a/webapp/packages/core-events/src/INotification.ts b/webapp/packages/core-events/src/INotification.ts index 6ea55579a2..1ebf9b8712 100644 --- a/webapp/packages/core-events/src/INotification.ts +++ b/webapp/packages/core-events/src/INotification.ts @@ -9,34 +9,50 @@ export enum ENotificationType { Info = 'Info', Error = 'Error', + Success = 'Success', Custom =' Custom' } -export type NotificationComponentProps = { - notification: INotification; +export type INotificationExtraProps = Record & { + source?: T; +} + +export type NotificationComponentProps< + TSource = undefined, + TProps extends INotificationExtraProps = INotificationExtraProps> = { + notification: INotification; onClose: () => void; } -export type NotificationComponent = React.FunctionComponent> -export interface INotification { +export type NotificationComponent< + TSource = undefined, + TProps extends INotificationExtraProps = INotificationExtraProps, +> = React.FunctionComponent & TProps> + +export interface INotification< + TSource = undefined, + TProps extends INotificationExtraProps = INotificationExtraProps> { readonly id: number; type: ENotificationType; title: string; message?: string; details?: string | Error; + persistent?: boolean; isSilent: boolean; - customComponent?: () => NotificationComponent; - source: T; + extraProps: TProps; + customComponent?: () => NotificationComponent; close: () => void; showDetails: () => void; } -export interface INotificationOptions { +export interface INotificationOptions< + TSource = undefined, + TProps extends INotificationExtraProps = INotificationExtraProps> { title: string; message?: string; details?: string | Error; isSilent?: boolean; persistent?: boolean; - customComponent?: () => NotificationComponent; - source?: T; + extraProps?: TProps; + customComponent?: () => NotificationComponent; } diff --git a/webapp/packages/core-events/src/NotificationService.ts b/webapp/packages/core-events/src/NotificationService.ts index cf73942a60..308bce0466 100644 --- a/webapp/packages/core-events/src/NotificationService.ts +++ b/webapp/packages/core-events/src/NotificationService.ts @@ -12,51 +12,70 @@ import { OrderedMap } from '@cloudbeaver/core-utils'; import { EventsSettingsService } from './EventsSettingsService'; import { - ENotificationType, INotification, INotificationOptions, NotificationComponent + ENotificationType, INotification, INotificationExtraProps, INotificationOptions, NotificationComponent } from './INotification'; @injectable() export class NotificationService { - readonly notificationList = new OrderedMap>(({ id }) => id); + // todo change to common new Map() + readonly notificationList = new OrderedMap>(({ id }) => id); private notificationNextId = 0 constructor( private settings: EventsSettingsService, ) {} - notify(options: INotificationOptions, type: ENotificationType) { + notify = Record>( + options: INotificationOptions, type: ENotificationType + ) { + if (options.persistent) { + const persistentNotifications = this.notificationList.values.filter(value => value.persistent); + if (persistentNotifications.length >= this.settings.settings.getValue('maxPersistentAllow')) { + throw new Error(`You cannot create more than ${this.settings.settings.getValue('maxPersistentAllow')} persistent notification`); + } + } + const id = this.notificationNextId++; - const notification: INotification = { + const notification: INotification = { id, title: options.title, message: options.message, details: options.details, isSilent: !!options.isSilent, customComponent: options.customComponent, - source: options.source!, + extraProps: options.extraProps || {} as TProps, + persistent: options.persistent, type, close: this.close.bind(this, id), showDetails: this.showDetails.bind(this, id), }; + this.notificationList.addValue(notification); - if (this.notificationList.values.length > this.settings.settings.getValue('notificationsPool')) { - this.notificationList.remove(this.notificationList.keys[0]); + const filteredNotificationList = this.notificationList.values.filter(notification => !notification.persistent); + + if (filteredNotificationList.length > this.settings.settings.getValue('notificationsPool')) { + let i = 0; + while (this.notificationList.get(this.notificationList.keys[i])?.persistent) { + i++; + } + this.notificationList.remove(this.notificationList.keys[i]); } } - customNotification( - component: () => NotificationComponent, - source?: T, - options?: INotificationOptions + customNotification = Record>( + component: () => NotificationComponent, + props?: TProps & INotificationExtraProps, + options?: INotificationOptions & { type?: ENotificationType; } ) { this.notify({ title: '', ...options, customComponent: component, - source, - }, ENotificationType.Custom); + extraProps: props || {} as TProps, + }, options?.type ?? ENotificationType.Custom); } logInfo(notification: INotificationOptions) { diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index b683116dbf..599e8608cb 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -18,7 +18,7 @@ export const defaultRULocale = [ ['ui_data_saving_error', 'Ошибка при сохранении изменений'], ['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'], ['root_permission_denied', 'Отказано в доступе'], - ['app_root_session_expired_title', 'Сесстия истекла'], + ['app_root_session_expired_title', 'Сессия истекла'], ['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'], ['app_root_session_expired_reload', 'Перезагрузить'], ['core_eventsLog_dbeaverErrorDetails', 'Детали ошибки'], diff --git a/webapp/packages/core-notifications/src/NotificationsItem/NotificationItemController.ts b/webapp/packages/core-notifications/src/NotificationsItem/NotificationItemController.ts index d6f973aad8..7fbaa17ee3 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/NotificationItemController.ts +++ b/webapp/packages/core-notifications/src/NotificationsItem/NotificationItemController.ts @@ -31,7 +31,7 @@ export class NotificationItemController implements IInitializableController { this.handleShowDetails = this.showDetails.bind(this); } - if (notification.type !== ENotificationType.Error) { + if (notification.type !== ENotificationType.Error && !notification.persistent) { this.closeAfter = FadeTimeout; } } diff --git a/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx b/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx index 9a06908cd0..7d37bf6bcc 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx +++ b/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx @@ -26,7 +26,7 @@ export const NotificationsItem = observer(function Notification({ notification } if (notification.customComponent) { const Custom = notification.customComponent(); - return ; + return ; } return ( @@ -34,9 +34,9 @@ export const NotificationsItem = observer(function Notification({ notification } text={notification.title} type={notification.type} disableShowDetails={controller.isDetailsDialogOpen} + closeAfter={controller.closeAfter} onClose={controller.handleClose} onShowDetails={controller.handleShowDetails} - closeAfter={controller.closeAfter} /> ); }); diff --git a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/NotificationMark.tsx b/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/NotificationMark.tsx index 57b6e194b4..86e356ebcf 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/NotificationMark.tsx +++ b/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/NotificationMark.tsx @@ -18,8 +18,10 @@ type IconProps = HTMLProps & { export function NotificationMark({ type, ...props }: IconProps) { return (
+ {/* todo change to info icon */} {type === ENotificationType.Info && } {type === ENotificationType.Error && } + {type === ENotificationType.Success && }
); } diff --git a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/Snackbar.tsx b/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/Snackbar.tsx index c21477c028..51e33dbb4f 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/Snackbar.tsx +++ b/webapp/packages/core-notifications/src/NotificationsItem/Snackbar/Snackbar.tsx @@ -9,13 +9,12 @@ import { useEffect, useState } from 'react'; import styled, { use } from 'reshadow'; -import { Button, IconButton } from '@cloudbeaver/core-blocks'; +import { Button, IconButton, SNACKBAR_COMMON_STYLES } from '@cloudbeaver/core-blocks'; import { ENotificationType } from '@cloudbeaver/core-events'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import { NotificationMark } from './NotificationMark'; -import { SNACKBAR_STYLES } from './styles'; type SnackbarProps = { type?: ENotificationType; @@ -34,7 +33,7 @@ export function Snackbar({ onClose, onShowDetails, }: SnackbarProps) { - const styles = useStyles(SNACKBAR_STYLES); + const styles = useStyles(SNACKBAR_COMMON_STYLES); const [mounted, setMounted] = useState(false); const [closing, setClosing] = useState(false); const translate = useTranslate(); @@ -60,12 +59,12 @@ export function Snackbar({ }, []); return styled(styles)( - + {type && } {translate(text)} {!closeAfter && onClose && ( - + )} @@ -74,8 +73,8 @@ export function Snackbar({ diff --git a/webapp/packages/core-notifications/src/index.ts b/webapp/packages/core-notifications/src/index.ts index 5afabefcd1..c1be625de8 100644 --- a/webapp/packages/core-notifications/src/index.ts +++ b/webapp/packages/core-notifications/src/index.ts @@ -1,4 +1,4 @@ -export * from './NotificationsItem/Snackbar/styles'; export * from './NotificationsItem/Snackbar/NotificationMark'; export * from './NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog'; export * from './Notifications'; +export * from './NotificationsItem/Snackbar/NotificationMark'; diff --git a/webapp/packages/plugin-data-export/src/DataExportService.ts b/webapp/packages/plugin-data-export/src/DataExportService.ts index e6a8945c71..dc01b98b48 100644 --- a/webapp/packages/plugin-data-export/src/DataExportService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportService.ts @@ -7,7 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; import { DataTransferParameters } from '@cloudbeaver/core-sdk'; import { DataExportProcessService } from './DataExportProcessService'; @@ -48,7 +48,7 @@ export class DataExportService { parameters ); - this.notificationService.customNotification(() => ExportNotification, taskId); + this.notificationService.customNotification(() => ExportNotification, { source: taskId }); return taskId; } } diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx index 806580dfd6..5a30c513e5 100644 --- a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx +++ b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx @@ -10,11 +10,12 @@ import { observer } from 'mobx-react'; import { useEffect, useState } from 'react'; import styled, { use, css } from 'reshadow'; -import { Button, Loader, IconButton } from '@cloudbeaver/core-blocks'; +import { + Button, Loader, IconButton, SNACKBAR_COMMON_STYLES +} from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { NotificationComponentProps } from '@cloudbeaver/core-events'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { SNACKBAR_STYLES } from '@cloudbeaver/core-notifications'; import { useStyles } from '@cloudbeaver/core-theming'; import { EDeferredState } from '@cloudbeaver/core-utils'; @@ -50,13 +51,13 @@ export const ExportNotification = observer(function ExportNotification({ setMounted(true); }, []); - return styled(useStyles(SNACKBAR_STYLES, styles))( - + return styled(useStyles(SNACKBAR_COMMON_STYLES, styles))( + {translate(controller.status)} {!controller.isPending && ( - + )} @@ -78,8 +79,8 @@ export const ExportNotification = observer(function ExportNotification({ tag='a' href={controller.downloadUrl} mod={['unelevated']} - onClick={controller.download} download + onClick={controller.download} > {translate('data_transfer_notification_download')} @@ -89,8 +90,8 @@ export const ExportNotification = observer(function ExportNotification({ @@ -99,8 +100,8 @@ export const ExportNotification = observer(function ExportNotification({ diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts index 4c1e58eb87..83320d2540 100644 --- a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts +++ b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts @@ -35,7 +35,7 @@ export class ExportNotificationController implements IInitializableController { } get task() { - return this.dataExportProcessService.exportProcesses.get(this.notification.source); + return this.dataExportProcessService.exportProcesses.get(this.notification?.extraProps?.source); } get hasDetails() { @@ -69,7 +69,7 @@ export class ExportNotificationController implements IInitializableController { } get downloadUrl() { - return this.dataExportProcessService.downloadUrl(this.notification.source); + return this.dataExportProcessService.downloadUrl(this.notification.extraProps.source); } private notification!: INotification; @@ -86,17 +86,17 @@ export class ExportNotificationController implements IInitializableController { } delete = () => { - this.dataExportProcessService.delete(this.notification.source); + this.dataExportProcessService.delete(this.notification.extraProps.source); this.notification.close(); } download = () => { - this.dataExportProcessService.download(this.notification.source); + this.dataExportProcessService.download(this.notification.extraProps.source); this.notification.close(); } cancel = () => { - this.dataExportProcessService.cancel(this.notification.source); + this.dataExportProcessService.cancel(this.notification.extraProps.source); } showDetails= async () => {