Skip to content

Commit 4624845

Browse files
fix(PDiskPage): fix error boundary on failed restart (#2069)
1 parent 74355a4 commit 4624845

File tree

6 files changed

+128
-37
lines changed

6 files changed

+128
-37
lines changed

src/components/CriticalActionDialog/CriticalActionDialog.tsx

+15-9
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,28 @@ import {Checkbox, Dialog, Icon} from '@gravity-ui/uikit';
66
import {ResultIssues} from '../../containers/Tenant/Query/Issues/Issues';
77
import type {IResponseError} from '../../types/api/error';
88
import {cn} from '../../utils/cn';
9+
import {isResponseError, isResponseErrorWithIssues} from '../../utils/response';
910

1011
import {criticalActionDialogKeyset} from './i18n';
1112

1213
import './CriticalActionDialog.scss';
1314

1415
const b = cn('ydb-critical-dialog');
1516

16-
const parseError = (error: IResponseError) => {
17-
if (error.data && 'issues' in error.data && error.data.issues) {
18-
return <ResultIssues hideSeverity data={error.data} />;
19-
}
20-
if (error.status === 403) {
21-
return criticalActionDialogKeyset('no-rights-error');
22-
}
23-
if (error.statusText) {
24-
return error.statusText;
17+
const parseError = (error: unknown) => {
18+
if (isResponseError(error)) {
19+
if (error.status === 403) {
20+
return criticalActionDialogKeyset('no-rights-error');
21+
}
22+
if (typeof error.data === 'string') {
23+
return error.data;
24+
}
25+
if (isResponseErrorWithIssues(error) && error.data) {
26+
return <ResultIssues hideSeverity data={error.data} />;
27+
}
28+
if (error.statusText) {
29+
return error.statusText;
30+
}
2531
}
2632

2733
return criticalActionDialogKeyset('default-error');

src/containers/Tenant/Query/utils/isQueryCancelledError.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type {IResponseError} from '../../../../types/api/error';
21
import {isQueryErrorResponse, parseQueryError} from '../../../../utils/query';
2+
import {isResponseError} from '../../../../utils/response';
33

44
function isAbortError(error: unknown): error is {name: string} {
55
return (
@@ -10,10 +10,6 @@ function isAbortError(error: unknown): error is {name: string} {
1010
);
1111
}
1212

13-
function isResponseError(error: unknown): error is IResponseError {
14-
return typeof error === 'object' && error !== null && 'isCancelled' in error;
15-
}
16-
1713
export function isQueryCancelledError(error: unknown): boolean {
1814
if (isAbortError(error)) {
1915
return true;

src/types/api/error.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import type {TIssueMessage} from './operations';
2-
3-
// TODO: extend with other error types
4-
type ResponseErrorData = TIssueMessage;
5-
6-
export interface IResponseError<T = ResponseErrorData> {
1+
export interface IResponseError<T = unknown> {
72
data?: T;
83
status?: number;
94
statusText?: string;

src/utils/__test__/response.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {isResponseError, isResponseErrorWithIssues} from '../response';
2+
3+
describe('isResponseError', () => {
4+
test('should return false on incorrect data', () => {
5+
const incorrectValues = [{}, [], 'hello', 123, null, undefined];
6+
7+
incorrectValues.forEach((value) => {
8+
expect(isResponseError(value)).toBe(false);
9+
});
10+
});
11+
test('should return true if it is object with status or status text', () => {
12+
expect(isResponseError({status: 403})).toBe(true);
13+
expect(isResponseError({statusText: 'Gateway timeout'})).toBe(true);
14+
});
15+
test('should return true if it is cancelled', () => {
16+
expect(isResponseError({isCancelled: true})).toBe(true);
17+
});
18+
test('should return true if it has data', () => {
19+
expect(isResponseError({data: 'Everything is broken'})).toBe(true);
20+
});
21+
});
22+
23+
describe('isResponseErrorWithIssues', () => {
24+
test('should return false on incorrect data', () => {
25+
const incorrectValues = [{}, [], 'hello', 123, null, undefined];
26+
27+
incorrectValues.forEach((value) => {
28+
expect(isResponseErrorWithIssues({data: value})).toBe(false);
29+
});
30+
});
31+
test('should return false on empty issues', () => {
32+
expect(
33+
isResponseErrorWithIssues({
34+
data: {issues: []},
35+
}),
36+
).toBe(false);
37+
});
38+
test('should return false on incorrect issues value', () => {
39+
const incorrectValues = [{}, [], 'hello', 123, null, undefined];
40+
41+
incorrectValues.forEach((value) => {
42+
expect(isResponseErrorWithIssues({data: {issues: value}})).toBe(false);
43+
});
44+
});
45+
test('should return false on incorrect issue inside issues', () => {
46+
const incorrectValues = [{}, [], 'hello', 123, null, undefined];
47+
48+
incorrectValues.forEach((value) => {
49+
expect(isResponseErrorWithIssues({data: {issues: [value]}})).toBe(false);
50+
});
51+
});
52+
test('should return true if it is an array of issues', () => {
53+
expect(
54+
isResponseErrorWithIssues({
55+
data: {issues: [{message: 'Some error'}]},
56+
}),
57+
).toBe(true);
58+
});
59+
});

src/utils/errors/index.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type {IResponseError} from '../../types/api/error';
2-
import {isNetworkError} from '../response';
1+
import {isNetworkError, isResponseError} from '../response';
32

43
import i18n from './i18n';
54

@@ -24,12 +23,16 @@ export function prepareCommonErrorMessage(err: unknown): string {
2423
return err.message;
2524
}
2625

27-
if (typeof err === 'object' && 'data' in err) {
28-
const responseError = err as IResponseError;
29-
if (responseError.data?.message) {
30-
return responseError.data.message;
31-
} else if (typeof responseError.data === 'string') {
32-
return responseError.data;
26+
if (isResponseError(err)) {
27+
if (
28+
err.data &&
29+
typeof err.data === 'object' &&
30+
'message' in err.data &&
31+
typeof err.data.message === 'string'
32+
) {
33+
return err.data.message;
34+
} else if (typeof err.data === 'string') {
35+
return err.data;
3336
}
3437
}
3538

src/utils/response.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import type {AxiosError, AxiosResponse} from 'axios';
22

3-
import type {NetworkError} from '../types/api/error';
3+
import type {IResponseError, NetworkError} from '../types/api/error';
4+
import type {TIssueMessage} from '../types/api/operations';
5+
import type {IssueMessage} from '../types/api/query';
6+
7+
export function isResponseError(error: unknown): error is IResponseError {
8+
if (!error || typeof error !== 'object') {
9+
return false;
10+
}
11+
const hasData = 'data' in error;
12+
const hasStatus = 'status' in error && typeof error.status === 'number';
13+
const hasStatusText = 'statusText' in error && typeof error.statusText === 'string';
14+
const isCancelled = 'isCancelled' in error && typeof error.isCancelled === 'boolean';
15+
16+
return hasData || hasStatus || hasStatusText || isCancelled;
17+
}
418

519
export const isNetworkError = (error: unknown): error is NetworkError => {
620
return Boolean(
@@ -26,24 +40,42 @@ export function isAxiosError(error: unknown): error is AxiosErrorObject {
2640
);
2741
}
2842

29-
export function isAccessError(error: unknown): error is {status: number} {
30-
return Boolean(
31-
error &&
32-
typeof error === 'object' &&
33-
'status' in error &&
34-
(error.status === 403 || error.status === 401),
35-
);
43+
export function isAccessError(error: unknown): error is IResponseError {
44+
return Boolean(isResponseError(error) && (error.status === 403 || error.status === 401));
3645
}
3746

3847
export function isRedirectToAuth(error: unknown): error is {status: 401; data: {authUrl: string}} {
3948
return Boolean(
4049
isAccessError(error) &&
4150
error.status === 401 &&
42-
'data' in error &&
4351
error.data &&
4452
typeof error.data === 'object' &&
4553
'authUrl' in error.data &&
4654
error.data.authUrl &&
4755
typeof error.data.authUrl === 'string',
4856
);
4957
}
58+
59+
type Issue = TIssueMessage | IssueMessage;
60+
61+
export function isResponseErrorWithIssues(
62+
error: unknown,
63+
): error is IResponseError<{issues: Issue[]}> {
64+
return Boolean(
65+
isResponseError(error) &&
66+
error.data &&
67+
typeof error.data === 'object' &&
68+
'issues' in error.data &&
69+
isIssuesArray(error.data.issues),
70+
);
71+
}
72+
73+
export function isIssuesArray(arr: unknown): arr is Issue[] {
74+
return Boolean(Array.isArray(arr) && arr.length && arr.every(isIssue));
75+
}
76+
77+
export function isIssue(obj: unknown): obj is Issue {
78+
return Boolean(
79+
obj && typeof obj === 'object' && 'message' in obj && typeof obj.message === 'string',
80+
);
81+
}

0 commit comments

Comments
 (0)