Merge pull request #167 from dbeaver/feature/expired-session-snackbar

Feature/expired session snackbar
This commit is contained in:
Alexey
2020-10-05 02:30:43 +03:00
committed by GitHub
21 changed files with 166 additions and 60 deletions
+1
View File
@@ -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';
@@ -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;
}
@@ -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;
@@ -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<NotificationComponentProps & ExtraProps> = 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)(
<notification as="div" {...use({ mounted })}>
<notification-header as="div">
<NotificationMark type={notification.type} />
<message as="div">{translate(notification.title)}</message>
{!notification.persistent && (
<IconButton
name="cross"
viewBox="0 0 16 16"
onClick={notification.close}
/>
)}
</notification-header>
<notification-body as="div">
<actions as="div">
<Button type="button" mod={['outlined']} onClick={onAction}>
{translate(actionText)}
</Button>
</actions>
</notification-body>
</notification>
);
};
@@ -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;
+2
View File
@@ -33,3 +33,5 @@ export * from './TextPlaceholder';
export * from './useClipboard';
export * from './useFocus';
export * from './BlocksLocaleService';
export * from './Snackbars/ActionSnackbar';
export * from './Snackbars/SnackbarCommonStyles';
@@ -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';
@@ -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';
@@ -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';
@@ -11,6 +11,8 @@ import { PluginManagerService } from '@cloudbeaver/core-plugin';
const defaultSettings = {
notificationsPool: 5,
maxPersistentAllow: 1,
};
export type EventsSettings = typeof defaultSettings
@@ -9,34 +9,50 @@
export enum ENotificationType {
Info = 'Info',
Error = 'Error',
Success = 'Success',
Custom =' Custom'
}
export type NotificationComponentProps<T = undefined> = {
notification: INotification<T>;
export type INotificationExtraProps<T> = Record<string, any> & {
source?: T;
}
export type NotificationComponentProps<
TSource = undefined,
TProps extends INotificationExtraProps<TSource> = INotificationExtraProps<TSource>> = {
notification: INotification<TSource, TProps>;
onClose: () => void;
}
export type NotificationComponent<T = undefined> = React.FunctionComponent<NotificationComponentProps<T>>
export interface INotification<T = undefined> {
export type NotificationComponent<
TSource = undefined,
TProps extends INotificationExtraProps<TSource> = INotificationExtraProps<TSource>,
> = React.FunctionComponent<NotificationComponentProps<TSource> & TProps>
export interface INotification<
TSource = undefined,
TProps extends INotificationExtraProps<TSource> = INotificationExtraProps<TSource>> {
readonly id: number;
type: ENotificationType;
title: string;
message?: string;
details?: string | Error;
persistent?: boolean;
isSilent: boolean;
customComponent?: () => NotificationComponent<T>;
source: T;
extraProps: TProps;
customComponent?: () => NotificationComponent<TSource, TProps>;
close: () => void;
showDetails: () => void;
}
export interface INotificationOptions<T = undefined> {
export interface INotificationOptions<
TSource = undefined,
TProps extends INotificationExtraProps<TSource> = INotificationExtraProps<TSource>> {
title: string;
message?: string;
details?: string | Error;
isSilent?: boolean;
persistent?: boolean;
customComponent?: () => NotificationComponent<T>;
source?: T;
extraProps?: TProps;
customComponent?: () => NotificationComponent<TSource, TProps>;
}
@@ -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<number, INotification<any>>(({ id }) => id);
// todo change to common new Map()
readonly notificationList = new OrderedMap<number, INotification<any, any>>(({ id }) => id);
private notificationNextId = 0
constructor(
private settings: EventsSettingsService,
) {}
notify<T = never>(options: INotificationOptions<T>, type: ENotificationType) {
notify<TSource = never, TProps extends INotificationExtraProps<
TSource> = Record<string, any>>(
options: INotificationOptions<TSource, TProps>, 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<T> = {
const notification: INotification<TSource, TProps> = {
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<T = never>(
component: () => NotificationComponent<T>,
source?: T,
options?: INotificationOptions<T>
customNotification<TSource = never, TProps extends INotificationExtraProps<TSource> = Record<string, any>>(
component: () => NotificationComponent<TSource, TProps>,
props?: TProps & INotificationExtraProps<TSource>,
options?: INotificationOptions<TSource, TProps> & { type?: ENotificationType; }
) {
this.notify({
title: '',
...options,
customComponent: component,
source,
}, ENotificationType.Custom);
extraProps: props || {} as TProps,
}, options?.type ?? ENotificationType.Custom);
}
logInfo<T>(notification: INotificationOptions<T>) {
@@ -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', 'Детали ошибки'],
@@ -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;
}
}
@@ -26,7 +26,7 @@ export const NotificationsItem = observer(function Notification({ notification }
if (notification.customComponent) {
const Custom = notification.customComponent();
return <Custom notification={notification} onClose={controller.handleClose} />;
return <Custom notification={notification} onClose={controller.handleClose} {...notification.extraProps} />;
}
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}
/>
);
});
@@ -18,8 +18,10 @@ type IconProps = HTMLProps<HTMLDivElement> & {
export function NotificationMark({ type, ...props }: IconProps) {
return (
<div {...props}>
{/* todo change to info icon */}
{type === ENotificationType.Info && <CheckCircle />}
{type === ENotificationType.Error && <Frown />}
{type === ENotificationType.Success && <CheckCircle />}
</div>
);
}
@@ -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)(
<notification as="div" {...use({ mounted, closing })} >
<notification as="div" {...use({ mounted, closing })}>
<notification-header as="div">
{type && <NotificationMark type={type} />}
<message as="div">{translate(text)}</message>
{!closeAfter && onClose && (
<IconButton onClick={onClose} name="cross" viewBox="0 0 16 16" />
<IconButton name="cross" viewBox="0 0 16 16" onClick={onClose} />
)}
</notification-header>
<notification-body as="div">
@@ -74,8 +73,8 @@ export function Snackbar({
<Button
type="button"
mod={['outlined']}
onClick={onShowDetails}
disabled={disableShowDetails}
onClick={onShowDetails}
>
{translate('ui_errors_details')}
</Button>
@@ -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';
@@ -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;
}
}
@@ -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))(
<notification as="div" {...use({ mounted })} >
return styled(useStyles(SNACKBAR_COMMON_STYLES, styles))(
<notification as="div" {...use({ mounted })}>
<notification-header as="div">
<Loader loading={controller.isPending} hideMessage />
<message as="div">{translate(controller.status)}</message>
{!controller.isPending && (
<IconButton onClick={controller.delete} name="cross" viewBox="0 0 16 16" />
<IconButton name="cross" viewBox="0 0 16 16" onClick={controller.delete} />
)}
</notification-header>
<notification-body as="div">
@@ -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')}
</Button>
@@ -89,8 +90,8 @@ export const ExportNotification = observer(function ExportNotification({
<Button
type="button"
mod={['outlined']}
onClick={controller.showDetails}
disabled={controller.isDetailsDialogOpen}
onClick={controller.showDetails}
>
{translate('ui_errors_details')}
</Button>
@@ -99,8 +100,8 @@ export const ExportNotification = observer(function ExportNotification({
<Button
type="button"
mod={['outlined']}
onClick={controller.cancel}
disabled={controller.process?.getState() === EDeferredState.CANCELLING}
onClick={controller.cancel}
>
{translate('ui_processing_cancel')}
</Button>
@@ -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<any>;
@@ -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 () => {