diff --git a/packages/ui/src/ui/assets/img/svg/500.svg b/packages/ui/src/ui/assets/img/svg/500.svg new file mode 100644 index 000000000..395b40fe8 --- /dev/null +++ b/packages/ui/src/ui/assets/img/svg/500.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/ui/src/ui/assets/img/svg/901.svg b/packages/ui/src/ui/assets/img/svg/901.svg new file mode 100644 index 000000000..9c6a2c26f --- /dev/null +++ b/packages/ui/src/ui/assets/img/svg/901.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.js b/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.js index 6a0b98d6e..e91d18656 100644 --- a/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.js +++ b/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.js @@ -113,12 +113,7 @@ export default class ErrorDetails extends Component { return (
- +
); } @@ -219,7 +214,8 @@ export default class ErrorDetails extends Component { return ( ); } diff --git a/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.scss b/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.scss index ab86d075e..7229c8d36 100644 --- a/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.scss +++ b/packages/ui/src/ui/components/ErrorDetails/ErrorDetails.scss @@ -1,12 +1,15 @@ .elements-error-details { &__details { + display: flex; + flex-direction: column; + gap: 10px; + + padding: 8px; margin-bottom: 10px; - .unipika-wrapper { - background-color: var(--main-background); + border-radius: var(--g-border-radius-l); - padding: 10px 15px; - } + background-color: var(--light-background); } &__message { @@ -17,7 +20,7 @@ margin-bottom: 5px; &-toggler { - margin-right: 5px; + margin-right: 4px; } } @@ -27,7 +30,14 @@ } &__tabs { - margin: 10px 0 20px; + span { + color: var(--g-color-text-complementary); + font-weight: 700; + } + + ::before { + display: none; + } } &__inner-errors { diff --git a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js index 787c85246..3d34be2c4 100644 --- a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js +++ b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js @@ -7,7 +7,6 @@ import cn from 'bem-cn-lite'; import map_ from 'lodash/map'; -import ypath from '../../../common/thor/ypath'; import {getCluster} from '../../../store/selectors/global'; import {updateTitle} from '../../../store/actions/global'; @@ -18,13 +17,11 @@ import { MoveObjectModal, RestoreObjectModal, } from './PathEditorModal'; -import RequestPermissions from '../../../pages/navigation/tabs/ACL/RequestPermissions/RequestPermissions'; import ErrorBoundary from '../../../components/ErrorBoundary/ErrorBoundary'; import ContentViewer from './ContentViewer/ContentViewer'; import {checkContentIsSupported} from './ContentViewer/helpers'; -import Error from '../../../components/Error/Error'; -import {Info} from '../../../components/Info/Info'; import Tabs from '../../../components/Tabs/Tabs'; +import NavigationError from './NavigationError'; import {Tab} from '../../../constants/navigation'; import {LOADING_STATUS} from '../../../constants/index'; @@ -57,8 +54,6 @@ import CreateACOModal from '../modals/CreateACOModal'; import Button from '../../../components/Button/Button'; import Icon from '../../../components/Icon/Icon'; import {showNavigationAttributesEditor} from '../../../store/actions/navigation/modals/attributes-editor'; -import {getPermissionDeniedError} from '../../../utils/errors'; -import {getParentPath} from '../../../utils/navigation'; import UIFactory from '../../../UIFactory'; import './Navigation.scss'; @@ -250,65 +245,12 @@ class Navigation extends Component { renderError() { const { error: {message, details}, - isIdmSupported, + cluster, + path, } = this.props; - // Looking for permission denied error - const permissionDeniedError = getPermissionDeniedError(details); - const isPermissionDenied = permissionDeniedError && isIdmSupported; - - return ( -
-
- - {isPermissionDenied && this.renderRequestPermission(permissionDeniedError)} -
-
- ); - } - - renderRequestPermission(error) { - const objectType = ypath.getValue(error?.attributes, '/object_type'); - const errorPath = ypath.getValue(error?.attributes, '/path'); - const {path: currentPath, cluster} = this.props; - const isRequestPermissionsForPathAllowed = objectType === 'map_node'; - - const path = errorPath ?? currentPath; - - const pathForRequest = isRequestPermissionsForPathAllowed ? path : getParentPath(path); - const textForRequest = isRequestPermissionsForPathAllowed - ? 'Request permission' - : 'Request permission for parent node'; - - return ( -
- {!isRequestPermissionsForPathAllowed && - this.renderRequestPermissionIsNotAllowed(objectType)} - - -
- ); - } - - renderRequestPermissionIsNotAllowed(objectType) { return ( - - It is not possible to request access to the{' '} - {hammer.format['Readable'](objectType, {caps: 'none'})}. Please request access to - the parent directory. - + ); } @@ -317,7 +259,7 @@ class Navigation extends Component { return ( -
+
@@ -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 };