feat(core-sdk): CB-1403 add quota error

This commit is contained in:
Naumov Alexey
2021-11-26 00:08:00 +03:00
parent 3105824315
commit 691c88b38c
11 changed files with 95 additions and 25 deletions
@@ -67,5 +67,6 @@ export const defaultENLocale = [
['app_root_session_expired_title', 'Session expired'],
['app_root_session_expired_message', 'Session has been expired. Do you want to reload?'],
['app_root_session_expired_reload', 'Reload'],
['app_root_quota_exceeded', 'Quota exceeded'],
['core_eventsLog_dbeaverErrorDetails', 'Error details'],
];
@@ -65,5 +65,6 @@ export const defaultRULocale = [
['app_root_session_expired_title', 'Сессия истекла'],
['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'],
['app_root_session_expired_reload', 'Перезагрузить'],
['app_root_quota_exceeded', 'Квота превышена'],
['core_eventsLog_dbeaverErrorDetails', 'Детали ошибки'],
];
@@ -9,8 +9,13 @@
import { DetailsError } from './DetailsError';
import type { ServerError } from './sdk';
export enum ServerErrorType {
'QUOTE_EXCEEDED' = 'quotaExceeded'
}
export class ServerInternalError extends DetailsError implements ServerError {
readonly errorCode?: string;
readonly errorType?: string;
readonly stackTrace?: string;
readonly causedBy?: ServerError;
@@ -18,6 +23,7 @@ export class ServerInternalError extends DetailsError implements ServerError {
super(error.message);
this.name = 'Server Internal Error';
this.errorCode = error.errorCode;
this.errorType = error.errorType;
this.stackTrace = error.stackTrace;
this.causedBy = error.causedBy;
}
@@ -7,11 +7,13 @@
*/
import { DetailsError } from './DetailsError';
import { ServerInternalError } from './ServerInternalError';
export interface IErrorDetails {
name: string;
message: string;
hasDetails: boolean;
errorType?: string;
}
export function hasDetails(error: Error): error is DetailsError {
@@ -20,9 +22,16 @@ export function hasDetails(error: Error): error is DetailsError {
export function getErrorDetails(error: Error | DetailsError): IErrorDetails {
const exceptionMessage = hasDetails(error) ? error.errorMessage : error.message || error.name;
return {
const details: IErrorDetails = {
name: error.name,
message: exceptionMessage,
hasDetails: hasDetails(error),
};
if (error instanceof ServerInternalError && error.errorType) {
details.errorType = error.errorType;
}
return details;
}
@@ -13,6 +13,7 @@ mutation getAsyncTaskInfo(
error {
message
errorCode
errorType
stackTrace
}
taskResult
+3 -1
View File
@@ -1133,6 +1133,7 @@ export interface ServerConfigInput {
export interface ServerError {
causedBy?: Maybe<ServerError>;
errorCode?: Maybe<Scalars['String']>;
errorType?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
stackTrace?: Maybe<Scalars['String']>;
}
@@ -1695,7 +1696,7 @@ export type GetAsyncTaskInfoMutationVariables = Exact<{
removeOnFinish: Scalars['Boolean'];
}>;
export interface GetAsyncTaskInfoMutation { taskInfo: { id: string; name?: Maybe<string>; running: boolean; status?: Maybe<string>; taskResult?: Maybe<any>; error?: Maybe<{ message?: Maybe<string>; errorCode?: Maybe<string>; stackTrace?: Maybe<string> }> } }
export interface GetAsyncTaskInfoMutation { taskInfo: { id: string; name?: Maybe<string>; running: boolean; status?: Maybe<string>; taskResult?: Maybe<any>; error?: Maybe<{ message?: Maybe<string>; errorCode?: Maybe<string>; errorType?: Maybe<string>; stackTrace?: Maybe<string> }> } }
export type GetNetworkHandlersQueryVariables = Exact<{ [key: string]: never }>;
@@ -2763,6 +2764,7 @@ export const GetAsyncTaskInfoDocument = `
error {
message
errorCode
errorType
stackTrace
}
taskResult
@@ -21,6 +21,14 @@ import { EDeferredState } from '@cloudbeaver/core-utils';
import { ExportNotificationController } from './ExportNotificationController';
const styles = css`
message {
font-size: 16px;
opacity: 0.8;
overflow: auto;
max-height: 100px;
word-break: break-word;
white-space: pre-line;
}
source-name {
composes: theme-typography--body2 from global;
padding-top: 16px;
@@ -44,14 +52,15 @@ export const ExportNotification = observer<Props>(function ExportNotification({
}) {
const controller = useController(ExportNotificationController, notification);
const translate = useTranslate();
const { title, status } = controller.status;
const { title, status, message } = controller.status;
return styled(useStyles(styles))(
<SnackbarWrapper unclosable={status === ENotificationType.Loading} onClose={controller.delete}>
<SnackbarStatus status={status} />
<SnackbarContent>
<SnackbarBody title={translate(title)}>
<source-name as="div">
{message && <message>{message}</message>}
<source-name>
{controller.sourceName}
{controller.task?.context.sourceName && (
<pre title={controller.task.context.sourceName}>
@@ -61,7 +70,7 @@ export const ExportNotification = observer<Props>(function ExportNotification({
</source-name>
</SnackbarBody>
<SnackbarFooter timestamp={notification.timestamp}>
{status === ENotificationType.Info && (
{status === ENotificationType.Info && controller.downloadUrl && (
<>
<Button
type="button"
@@ -14,6 +14,7 @@ import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { ENotificationType, INotification } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { ServerInternalError, ServerErrorType } from '@cloudbeaver/core-sdk';
import { Deferred, EDeferredState } from '@cloudbeaver/core-utils';
import { DataExportProcessService, ExportProcess } from '../DataExportProcessService';
@@ -21,6 +22,7 @@ import { DataExportProcessService, ExportProcess } from '../DataExportProcessSer
interface ExportNotificationStatus {
title: string;
status: ENotificationType;
message?: string;
}
@injectable()
export class ExportNotificationController implements IInitializableController {
@@ -67,8 +69,23 @@ export class ExportNotificationController implements IInitializableController {
return { title: 'ui_processing_canceling', status: ENotificationType.Loading };
case EDeferredState.RESOLVED:
return { title: 'data_transfer_notification_ready', status: ENotificationType.Info };
default:
return { title: 'data_transfer_notification_error', status: ENotificationType.Error };
case EDeferredState.CANCELLED:
return { title: 'data_transfer_notification_cancelled', status: ENotificationType.Info };
default: {
const error = this.process?.getRejectionReason();
let title = 'data_transfer_notification_error';
let status = ENotificationType.Error;
let message = '';
if (error instanceof ServerInternalError && error.errorType === ServerErrorType.QUOTE_EXCEEDED) {
title = 'app_root_quota_exceeded';
status = ENotificationType.Info;
message = error.message;
}
return { title, status, message };
}
}
}
@@ -108,7 +125,7 @@ export class ExportNotificationController implements IInitializableController {
this.dataExportProcessService.cancel(this.notification.extraProps.source);
};
showDetails= async (): Promise<void> => {
showDetails = async (): Promise<void> => {
this.isDetailsDialogOpen = true;
try {
this.notification.showDetails();
@@ -6,7 +6,8 @@ export default [
['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 is ready to download'],
['data_transfer_notification_error', 'Preparation failed or canceled'],
['data_transfer_notification_error', 'Data export failed'],
['data_transfer_notification_cancelled', 'Data export was cancelled'],
['data_transfer_notification_download', 'Download'],
['data_transfer_notification_delete', 'Delete'],
['data_transfer_exporting_table', 'Table:'],
@@ -6,7 +6,8 @@ export default [
['data_transfer_dialog_preparation', 'Подготавливаем файл к экспорту. Пожалуйста подождите...'],
['data_transfer_notification_preparation', 'Подготавливаем файл к загрузке. Пожалуйста подождите...'],
['data_transfer_notification_ready', 'Файл готов к загрузке'],
['data_transfer_notification_error', 'Подготовка файла к загрузке не удалась или была отменена'],
['data_transfer_notification_error', 'Не удалось экспортировать данные'],
['data_transfer_notification_cancelled', 'Экспорт данных был отменен'],
['data_transfer_notification_download', 'Загрузить'],
['data_transfer_notification_delete', 'Удалить'],
['data_transfer_exporting_table', 'Таблица:'],
@@ -12,6 +12,7 @@ import styled, { css, use } from 'reshadow';
import { Button, IconOrImage, useErrorDetails, useObservableRef, useStateDelay } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ServerErrorType } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel';
@@ -38,6 +39,7 @@ const style = composes(
transition: opacity 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out, background 0.3s ease-in-out;
&[|animated] {
overflow: hidden;
pointer-events: auto;
opacity: 1;
}
@@ -46,7 +48,14 @@ const style = composes(
width: 72px;
height: 72px;
background: transparent!important;
overflow: hidden;
& IconOrImage {
cursor: pointer;
}
& error-message, & controls {
display: none;
}
}
&[|errorHidden] {
pointer-events: none;
@@ -55,6 +64,7 @@ const style = composes(
error-body {
display: flex;
gap: 24px;
align-items: center;
margin-bottom: 24px;
}
error-message {
@@ -63,10 +73,13 @@ const style = composes(
IconOrImage {
width: 40px;
height: 40px;
cursor: pointer;
}
Button {
margin-right: 16px;
controls {
display: flex;
gap: 16px;
& > Button {
flex-shrink: 0;
}
}
`
);
@@ -112,24 +125,33 @@ export const TableError = observer<Props>(function TableViewer({
const animated = useStateDelay(!!errorInfo.error && !loading, 1);
const errorHidden = errorInfo.error === null;
const quote = error.details?.errorType === ServerErrorType.QUOTE_EXCEEDED;
let icon = '/icons/error_icon.svg';
if (quote) {
icon = '/icons/info_icon.svg';
}
return styled(useStyles(style))(
<error {...use({ animated, collapsed: !errorInfo.display, errorHidden })} className={className}>
<error-body>
<IconOrImage icon='/icons/error_icon.svg' onClick={() => errorInfo.show()} />
<IconOrImage icon={icon} title={error.details?.message} onClick={() => errorInfo.show()} />
<error-message>{error.details?.message}</error-message>
</error-body>
<Button type='button' mod={['outlined']} onClick={() => errorInfo.hide()}>
{translate('ui_error_close')}
</Button>
{error.details?.hasDetails && (
<Button type='button' mod={['outlined']} onClick={error.open}>
{translate('ui_errors_details')}
<controls>
<Button type='button' mod={['outlined']} onClick={() => errorInfo.hide()}>
{translate('ui_error_close')}
</Button>
)}
<Button type='button' mod={['unelevated']} onClick={() => model.retry()}>
{translate('ui_processing_retry')}
</Button>
{!quote && error.details?.hasDetails && (
<Button type='button' mod={['outlined']} onClick={error.open}>
{translate('ui_errors_details')}
</Button>
)}
<Button type='button' mod={['unelevated']} onClick={() => model.retry()}>
{translate('ui_processing_retry')}
</Button>
</controls>
</error>
);
});