+
@@ -328,11 +270,9 @@ class Navigation extends Component {
-
- {loaded && this.renderView()}
- {hasError && this.renderError()}
-
+
{loaded && this.renderView()}
+ {hasError && this.renderError()}
{UIFactory.yqlWidgetSetup?.renderWidget()}
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.scss b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.scss
index f43ff774e..d285061ce 100644
--- a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.scss
+++ b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.scss
@@ -1,8 +1,10 @@
.navigation {
min-width: 1200px;
- &__error-action-button {
- margin-top: 20px;
+ &_error {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
}
&__viewer {
@@ -31,11 +33,6 @@
margin-top: 20px;
}
- &__error-action-button {
- width: 100%;
- height: 38px;
- }
-
&__instruments {
display: flex;
margin-bottom: 15px;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.scss b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.scss
new file mode 100644
index 000000000..26a02ee33
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.scss
@@ -0,0 +1,29 @@
+.navigation-error {
+ flex-grow: 1;
+
+ &__info {
+ max-width: 650px;
+
+ color: var(--g-color-text-complementary);
+ font-size: 13px;
+ font-weight: 400;
+ }
+
+ &__title {
+ font-size: 17px;
+ line-height: 24px;
+ font-weight: 500;
+ color: var(--g-color-text-primary);
+ }
+
+ &__unexpected-error {
+ margin-top: 20px;
+ }
+
+ &__request-permissions-button {
+ --_--height: auto;
+ width: fit-content;
+
+ padding: 5px 12px;
+ }
+}
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.tsx b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.tsx
new file mode 100644
index 000000000..eac37be33
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import cn from 'bem-cn-lite';
+import {Flex, Text} from '@gravity-ui/uikit';
+
+import Error from '../../../../components/Error/Error';
+import NavigationErrorImage from './NavigationErrorImage';
+import ErrorDetails from '../../../../components/ErrorDetails/ErrorDetails';
+import RequestPermission from './RequestPermission';
+import {getPermissionDeniedError} from '../../../../utils/errors';
+import {YTError} from '../../../../../@types/types';
+import {getErrorCode, getErrorTitle} from './helpers';
+
+import './NavigationError.scss';
+
+const cnNavigationError = cn('navigation-error');
+
+type Props = {
+ path?: string;
+ details: YTError;
+ cluster: string;
+ message: string;
+};
+
+function CommonError(props: Props) {
+ const {details, path, cluster} = props;
+
+ const code = getErrorCode(details);
+ const error = code == 901 ? getPermissionDeniedError(details)! : details;
+
+ const title = getErrorTitle(error, path);
+
+ return (
+
+
+
+
+
+ {title}
+
+ {code === 901 && (
+
+ )}
+
+
+ );
+}
+
+function UnexpectedError(props: Props) {
+ const {details, message} = props;
+
+ return (
+
+ );
+}
+
+function NavigationError(props: Props) {
+ const {details} = props;
+
+ const code = getErrorCode(details);
+
+ return (
+ <>
+ {code !== undefined && [500, 901].includes(code) ? (
+
+ ) : (
+
+ )}
+ >
+ );
+}
+
+export default NavigationError;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationErrorImage.tsx b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationErrorImage.tsx
new file mode 100644
index 000000000..e913552c0
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationErrorImage.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import {Icon} from '@gravity-ui/uikit';
+import {SVGIconData} from '@gravity-ui/uikit/build/esm/components/Icon/types';
+
+import ErrorImage901 from '../../../../assets/img/svg/901.svg';
+import ErrorImage500 from '../../../../assets/img/svg/500.svg';
+import {ErrorCode} from './helpers';
+
+type Props = {
+ type: ErrorCode;
+};
+
+type ImageMap = {
+ [key in ErrorCode]: SVGIconData;
+};
+
+const ErrorImages: ImageMap = {
+ 500: ErrorImage500,
+ 901: ErrorImage901,
+};
+
+function NavigationErrorImage(props: Props) {
+ const {type} = props;
+
+ const ErrorImage = ErrorImages[type];
+
+ return
;
+}
+
+export default NavigationErrorImage;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.tsx b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.tsx
new file mode 100644
index 000000000..d9739f08f
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import ypath from '../../../../../common/thor/ypath';
+import {getParentPath} from '../../../../../utils/navigation';
+import RequestPermissions from '../../../tabs/ACL/RequestPermissions/RequestPermissions';
+import RequestPermissionIsNotAllowed from './RequestPermissionIsNotAllowed';
+import {YTError} from '../../../../../../@types/types';
+
+type Props = {
+ error: YTError;
+ path?: string;
+ cluster: string;
+ block: (mix: string) => string;
+};
+
+function RequestPermission(props: Props) {
+ const {path: currentPath, error, cluster, block} = props;
+ const objectType = ypath.getValue(error?.attributes, '/object_type');
+ const errorPath = ypath.getValue(error?.attributes, '/path');
+ const isRequestPermissionsForPathAllowed = objectType === 'map_node';
+
+ const path = errorPath ?? currentPath;
+
+ const pathForRequest = isRequestPermissionsForPathAllowed ? path : getParentPath(path);
+
+ return (
+
+ {!isRequestPermissionsForPathAllowed && (
+
+ )}
+
+
+
+ );
+}
+
+export default RequestPermission;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermissionIsNotAllowed.tsx b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermissionIsNotAllowed.tsx
new file mode 100644
index 000000000..c84c963a1
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermissionIsNotAllowed.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import hammer from '../../../../../common/hammer';
+import {Info} from '../../../../../components/Info/Info';
+
+type Props = {
+ block: (mix: string) => string;
+ objectType: any;
+};
+
+function RequestPermissionIsNotAllowed(props: Props) {
+ const {block, objectType} = props;
+
+ return (
+
+ It is not possible to request access to the{' '}
+ {hammer.format['Readable'](objectType, {caps: 'none'})}. Please request access to the
+ parent directory.
+
+ );
+}
+
+export default RequestPermissionIsNotAllowed;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/index.ts b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/index.ts
new file mode 100644
index 000000000..52465ba85
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/index.ts
@@ -0,0 +1,3 @@
+import RequestPermission from './RequestPermission';
+
+export default RequestPermission;
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/helpers.ts b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/helpers.ts
new file mode 100644
index 000000000..3fc5da3af
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/helpers.ts
@@ -0,0 +1,84 @@
+import {getYtErrorCode} from '../../../../../utils/errors';
+import {YTError} from '../../../../../../@types/types';
+import {YTErrors} from '../../../../../rum/constants';
+import {UnipikaValue} from '../../../../../components/Yson/StructuredYson/StructuredYsonTypes';
+
+export type ErrorName = keyof typeof YTErrors;
+
+/**
+ * should be: (typeof YTErrors)[keyof typeof YTErrors]
+ * after migrating javascript-wrapper on TS
+ */
+export type ErrorCode = 500 | 901;
+
+type NoAccessTitlePayload = {
+ username: string;
+ permissions: Array
;
+ path: string;
+};
+
+type NoPathTitlePayload = {
+ path: string;
+};
+
+type TitlePayload = NoAccessTitlePayload & NoPathTitlePayload;
+
+type ErrorInfo = {
+ [key in ErrorCode]: {
+ getTitle: (payload: TitlePayload) => string;
+ };
+};
+
+export const ErrorsInfo: ErrorInfo = {
+ 901: {
+ getTitle: (payload: NoAccessTitlePayload) => {
+ const {username, permissions, path} = payload;
+ const permission = permissions.map((perm: UnipikaValue) => perm.$value).join(' | ');
+ return `User ${username} does not have "${permission}" access to node "${path}"`;
+ },
+ },
+ 500: {
+ getTitle: (payload: NoPathTitlePayload) => {
+ const {path} = payload;
+ return `Path "${path}" does not exist`;
+ },
+ },
+};
+
+export function getErrorTitle(error: YTError, path?: string): string {
+ const {attributes} = error;
+
+ const code = getErrorCode(error);
+
+ if (!code) return 'An unexpected error occurred';
+
+ const title = ErrorsInfo[code].getTitle({
+ path: path || '',
+ username: attributes?.user.$value || '',
+ permissions: attributes?.permission || '',
+ });
+
+ return title;
+}
+
+export function getErrorCode(error: YTError): ErrorCode | undefined {
+ if (!isNaN(getYtErrorCode(error))) {
+ return getYtErrorCode(error);
+ }
+
+ if (!error.inner_errors) return;
+
+ const errors = error.inner_errors;
+
+ for (const inner_error of errors) {
+ if (!isNaN(getYtErrorCode(inner_error))) {
+ return getYtErrorCode(inner_error);
+ }
+
+ if (inner_error.inner_errors) {
+ errors.concat(inner_error.inner_errors);
+ }
+ }
+
+ return;
+}
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/index.ts b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/index.ts
new file mode 100644
index 000000000..c5f595cf9
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/index.ts
@@ -0,0 +1 @@
+export * from './helpers';
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/index.tsx b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/index.tsx
new file mode 100644
index 000000000..205918d7d
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/index.tsx
@@ -0,0 +1,3 @@
+import NavigationError from './NavigationError';
+
+export default NavigationError;
diff --git a/packages/ui/src/ui/rum/constants.ts b/packages/ui/src/ui/rum/constants.ts
index b0d0629be..64c3ff21e 100644
--- a/packages/ui/src/ui/rum/constants.ts
+++ b/packages/ui/src/ui/rum/constants.ts
@@ -4,10 +4,11 @@ import ytLib from '@ytsaurus/javascript-wrapper';
const yt = ytLib();
export const YTErrors = {
- NODE_DOES_NOT_EXIST: yt.codes.NODE_DOES_NOT_EXIST,
- PERMISSION_DENIED: yt.codes.PERMISSION_DENIED,
- NO_SUCH_TRANSACTION: yt.codes.NO_SUCH_TRANSACTION, // User transaction * has expired or was aborted
- OPERATION_JOBS_LIMIT_EXEEDED: yt.codes.OPERATION_JOBS_LIMIT_EXEEDED,
- OPERATION_FAILED_TO_PREPARE: yt.codes.OPERATION_FAILED_TO_PREPARE,
- CANCELLED: yt.codes.CANCELLED,
+ NODE_DOES_NOT_EXIST: yt.codes.NODE_DOES_NOT_EXIST, // 500
+ PERMISSION_DENIED: yt.codes.PERMISSION_DENIED, // 901
+ NO_SUCH_TRANSACTION: yt.codes.NO_SUCH_TRANSACTION, // User transaction * has expired or was aborted, code: 11000
+ OPERATION_JOBS_LIMIT_EXEEDED: yt.codes.OPERATION_JOBS_LIMIT_EXEEDED, // 215
+ OPERATION_FAILED_TO_PREPARE: yt.codes.OPERATION_FAILED_TO_PREPARE, // 216
+ CANCELLED: yt.codes.CANCELLED, // cancelled
+ NO_SUCH_USER: yt.codes.NO_SUCH_USER, // 900
};