+
@@ -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..6bf70c2a7
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.scss
@@ -0,0 +1,22 @@
+.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;
+ }
+}
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..9314a2e84
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationError.tsx
@@ -0,0 +1,66 @@
+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 {getErrorTitle, getLeadingErrorCode} from './helpers';
+
+import './NavigationError.scss';
+
+const block = cn('navigation-error');
+
+type Props = {
+ path?: string;
+ details: YTError;
+ cluster: string;
+ message: string;
+};
+
+function PrettyError(props: Props) {
+ const {details, path, cluster} = props;
+
+ const code = getLeadingErrorCode(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
;
+}
+
+export function NavigationError(props: Props) {
+ const {details} = props;
+
+ const code = getLeadingErrorCode(details);
+
+ return (
+ <>
+ {code !== undefined && [500, 901].includes(code) ? (
+
+ ) : (
+
+ )}
+ >
+ );
+}
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..c60d18527
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/NavigationErrorImage.tsx
@@ -0,0 +1,28 @@
+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,
+};
+
+export function NavigationErrorImage(props: Props) {
+ const {type} = props;
+
+ const ErrorImage = ErrorImages[type];
+
+ return
;
+}
diff --git a/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.scss b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.scss
new file mode 100644
index 000000000..8891a7afe
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.scss
@@ -0,0 +1,12 @@
+.request-permission {
+ &__request-permissions-button {
+ --_--height: auto;
+ width: fit-content;
+
+ padding: 5px 12px;
+ }
+}
+
+.request-permission-is-not-allowed {
+ margin-bottom: 12px;
+}
\ No newline at end of file
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..c7ff4136b
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermission.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import cn from 'bem-cn-lite';
+
+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';
+
+import './RequestPermission.scss';
+
+const block = cn('request-permission');
+
+type Props = {
+ error: YTError;
+ path?: string;
+ cluster: string;
+};
+
+export function RequestPermission(props: Props) {
+ const {path: currentPath, error, cluster} = 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 && (
+
+ )}
+
+
+
+ );
+}
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..9166b7fc7
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/RequestPermissionIsNotAllowed.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import cn from 'bem-cn-lite';
+
+import hammer from '../../../../../common/hammer';
+import {Info} from '../../../../../components/Info/Info';
+
+import './RequestPermission.scss';
+
+const block = cn('request-permission-is-not-allowed');
+
+type Props = {
+ objectType: any;
+};
+
+export function RequestPermissionIsNotAllowed(props: Props) {
+ const {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.
+
+ );
+}
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..1c8dd5fe0
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/RequestPermission/index.ts
@@ -0,0 +1 @@
+export {RequestPermission} from './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..c470d5338
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/helpers/helpers.ts
@@ -0,0 +1,87 @@
+import {getYtErrorCode} from '../../../../../utils/errors';
+import {YTError} from '../../../../../../@types/types';
+import {UnipikaValue} from '../../../../../components/Yson/StructuredYson/StructuredYsonTypes';
+
+/**
+ * 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 = getLeadingErrorCode(error);
+
+ if (!code) return 'An unexpected error occurred';
+
+ const title = ErrorsInfo[code].getTitle({
+ path: path || '',
+ username: attributes?.user.$value || '',
+ permissions: attributes?.permission || '',
+ });
+
+ return title;
+}
+
+/**
+ * returns first non-undefined error code,
+ * from root error to inner errors
+ */
+export function getLeadingErrorCode(error: YTError): ErrorCode | undefined {
+ const errorCode = getYtErrorCode(error);
+ if (!isNaN(errorCode)) {
+ return errorCode;
+ }
+
+ if (!error.inner_errors) return;
+
+ const errors = error.inner_errors;
+
+ for (const inner_error of errors) {
+ const innerErrorCode = getYtErrorCode(inner_error);
+ if (!isNaN(innerErrorCode)) {
+ return innerErrorCode;
+ }
+
+ 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.ts b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/index.ts
new file mode 100644
index 000000000..c6e2a0676
--- /dev/null
+++ b/packages/ui/src/ui/pages/navigation/Navigation/NavigationError/index.ts
@@ -0,0 +1 @@
+export {NavigationError} from './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
};