diff --git a/src/components/CriticalActionDialog/CriticalActionDialog.tsx b/src/components/CriticalActionDialog/CriticalActionDialog.tsx index 4e80453de8..d8ea3eadc2 100644 --- a/src/components/CriticalActionDialog/CriticalActionDialog.tsx +++ b/src/components/CriticalActionDialog/CriticalActionDialog.tsx @@ -6,6 +6,7 @@ import {Checkbox, Dialog, Icon} from '@gravity-ui/uikit'; import {ResultIssues} from '../../containers/Tenant/Query/Issues/Issues'; import type {IResponseError} from '../../types/api/error'; import {cn} from '../../utils/cn'; +import {isResponseError, isResponseErrorWithIssues} from '../../utils/response'; import {criticalActionDialogKeyset} from './i18n'; @@ -13,15 +14,20 @@ import './CriticalActionDialog.scss'; const b = cn('ydb-critical-dialog'); -const parseError = (error: IResponseError) => { - if (error.data && 'issues' in error.data && error.data.issues) { - return ; - } - if (error.status === 403) { - return criticalActionDialogKeyset('no-rights-error'); - } - if (error.statusText) { - return error.statusText; +const parseError = (error: unknown) => { + if (isResponseError(error)) { + if (error.status === 403) { + return criticalActionDialogKeyset('no-rights-error'); + } + if (typeof error.data === 'string') { + return error.data; + } + if (isResponseErrorWithIssues(error) && error.data) { + return ; + } + if (error.statusText) { + return error.statusText; + } } return criticalActionDialogKeyset('default-error'); diff --git a/src/containers/Tenant/Query/utils/isQueryCancelledError.ts b/src/containers/Tenant/Query/utils/isQueryCancelledError.ts index 5a4afef5c8..816dfed694 100644 --- a/src/containers/Tenant/Query/utils/isQueryCancelledError.ts +++ b/src/containers/Tenant/Query/utils/isQueryCancelledError.ts @@ -1,5 +1,5 @@ -import type {IResponseError} from '../../../../types/api/error'; import {isQueryErrorResponse, parseQueryError} from '../../../../utils/query'; +import {isResponseError} from '../../../../utils/response'; function isAbortError(error: unknown): error is {name: string} { return ( @@ -10,10 +10,6 @@ function isAbortError(error: unknown): error is {name: string} { ); } -function isResponseError(error: unknown): error is IResponseError { - return typeof error === 'object' && error !== null && 'isCancelled' in error; -} - export function isQueryCancelledError(error: unknown): boolean { if (isAbortError(error)) { return true; diff --git a/src/types/api/error.ts b/src/types/api/error.ts index ffc48c7f4b..e4c099dea7 100644 --- a/src/types/api/error.ts +++ b/src/types/api/error.ts @@ -1,9 +1,4 @@ -import type {TIssueMessage} from './operations'; - -// TODO: extend with other error types -type ResponseErrorData = TIssueMessage; - -export interface IResponseError { +export interface IResponseError { data?: T; status?: number; statusText?: string; diff --git a/src/utils/__test__/response.test.ts b/src/utils/__test__/response.test.ts new file mode 100644 index 0000000000..cb26bfc336 --- /dev/null +++ b/src/utils/__test__/response.test.ts @@ -0,0 +1,59 @@ +import {isResponseError, isResponseErrorWithIssues} from '../response'; + +describe('isResponseError', () => { + test('should return false on incorrect data', () => { + const incorrectValues = [{}, [], 'hello', 123, null, undefined]; + + incorrectValues.forEach((value) => { + expect(isResponseError(value)).toBe(false); + }); + }); + test('should return true if it is object with status or status text', () => { + expect(isResponseError({status: 403})).toBe(true); + expect(isResponseError({statusText: 'Gateway timeout'})).toBe(true); + }); + test('should return true if it is cancelled', () => { + expect(isResponseError({isCancelled: true})).toBe(true); + }); + test('should return true if it has data', () => { + expect(isResponseError({data: 'Everything is broken'})).toBe(true); + }); +}); + +describe('isResponseErrorWithIssues', () => { + test('should return false on incorrect data', () => { + const incorrectValues = [{}, [], 'hello', 123, null, undefined]; + + incorrectValues.forEach((value) => { + expect(isResponseErrorWithIssues({data: value})).toBe(false); + }); + }); + test('should return false on empty issues', () => { + expect( + isResponseErrorWithIssues({ + data: {issues: []}, + }), + ).toBe(false); + }); + test('should return false on incorrect issues value', () => { + const incorrectValues = [{}, [], 'hello', 123, null, undefined]; + + incorrectValues.forEach((value) => { + expect(isResponseErrorWithIssues({data: {issues: value}})).toBe(false); + }); + }); + test('should return false on incorrect issue inside issues', () => { + const incorrectValues = [{}, [], 'hello', 123, null, undefined]; + + incorrectValues.forEach((value) => { + expect(isResponseErrorWithIssues({data: {issues: [value]}})).toBe(false); + }); + }); + test('should return true if it is an array of issues', () => { + expect( + isResponseErrorWithIssues({ + data: {issues: [{message: 'Some error'}]}, + }), + ).toBe(true); + }); +}); diff --git a/src/utils/errors/index.ts b/src/utils/errors/index.ts index e63fa7b8d6..a209fe0c8f 100644 --- a/src/utils/errors/index.ts +++ b/src/utils/errors/index.ts @@ -1,5 +1,4 @@ -import type {IResponseError} from '../../types/api/error'; -import {isNetworkError} from '../response'; +import {isNetworkError, isResponseError} from '../response'; import i18n from './i18n'; @@ -24,12 +23,16 @@ export function prepareCommonErrorMessage(err: unknown): string { return err.message; } - if (typeof err === 'object' && 'data' in err) { - const responseError = err as IResponseError; - if (responseError.data?.message) { - return responseError.data.message; - } else if (typeof responseError.data === 'string') { - return responseError.data; + if (isResponseError(err)) { + if ( + err.data && + typeof err.data === 'object' && + 'message' in err.data && + typeof err.data.message === 'string' + ) { + return err.data.message; + } else if (typeof err.data === 'string') { + return err.data; } } diff --git a/src/utils/response.ts b/src/utils/response.ts index 1e2a5ec413..6ddbe7b220 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -1,6 +1,20 @@ import type {AxiosError, AxiosResponse} from 'axios'; -import type {NetworkError} from '../types/api/error'; +import type {IResponseError, NetworkError} from '../types/api/error'; +import type {TIssueMessage} from '../types/api/operations'; +import type {IssueMessage} from '../types/api/query'; + +export function isResponseError(error: unknown): error is IResponseError { + if (!error || typeof error !== 'object') { + return false; + } + const hasData = 'data' in error; + const hasStatus = 'status' in error && typeof error.status === 'number'; + const hasStatusText = 'statusText' in error && typeof error.statusText === 'string'; + const isCancelled = 'isCancelled' in error && typeof error.isCancelled === 'boolean'; + + return hasData || hasStatus || hasStatusText || isCancelled; +} export const isNetworkError = (error: unknown): error is NetworkError => { return Boolean( @@ -26,20 +40,14 @@ export function isAxiosError(error: unknown): error is AxiosErrorObject { ); } -export function isAccessError(error: unknown): error is {status: number} { - return Boolean( - error && - typeof error === 'object' && - 'status' in error && - (error.status === 403 || error.status === 401), - ); +export function isAccessError(error: unknown): error is IResponseError { + return Boolean(isResponseError(error) && (error.status === 403 || error.status === 401)); } export function isRedirectToAuth(error: unknown): error is {status: 401; data: {authUrl: string}} { return Boolean( isAccessError(error) && error.status === 401 && - 'data' in error && error.data && typeof error.data === 'object' && 'authUrl' in error.data && @@ -47,3 +55,27 @@ export function isRedirectToAuth(error: unknown): error is {status: 401; data: { typeof error.data.authUrl === 'string', ); } + +type Issue = TIssueMessage | IssueMessage; + +export function isResponseErrorWithIssues( + error: unknown, +): error is IResponseError<{issues: Issue[]}> { + return Boolean( + isResponseError(error) && + error.data && + typeof error.data === 'object' && + 'issues' in error.data && + isIssuesArray(error.data.issues), + ); +} + +export function isIssuesArray(arr: unknown): arr is Issue[] { + return Boolean(Array.isArray(arr) && arr.length && arr.every(isIssue)); +} + +export function isIssue(obj: unknown): obj is Issue { + return Boolean( + obj && typeof obj === 'object' && 'message' in obj && typeof obj.message === 'string', + ); +}