From 2b37173a57b51e66be5cdccd523ea53b2afb541d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 9 Sep 2025 17:09:23 +0300 Subject: [PATCH] feat: configure access errors --- src/components/EmptyState/EmptyState.scss | 25 +-- src/components/EmptyState/EmptyState.tsx | 36 ++-- src/components/Errors/403/AccessDenied.tsx | 17 +- .../Errors/PageError/PageError.scss | 7 + src/components/Errors/PageError/PageError.tsx | 37 +++- src/containers/App/Content.tsx | 11 +- src/containers/Clusters/Clusters.tsx | 187 +++++++++--------- src/containers/Header/Header.tsx | 12 +- src/containers/Tenant/Tenant.tsx | 6 +- src/uiFactory/types.ts | 2 + 10 files changed, 206 insertions(+), 134 deletions(-) create mode 100644 src/components/Errors/PageError/PageError.scss diff --git a/src/components/EmptyState/EmptyState.scss b/src/components/EmptyState/EmptyState.scss index b6c6930efc..7eb1644fe1 100644 --- a/src/components/EmptyState/EmptyState.scss +++ b/src/components/EmptyState/EmptyState.scss @@ -10,10 +10,7 @@ &__wrapper { display: grid; - grid-template-areas: - 'image title' - 'image description' - 'image actions'; + grid-template-areas: 'image content'; &_size_xs { width: 321px; @@ -28,8 +25,8 @@ } &_size_m { - width: 800px; - height: 240px; + width: 600px; + height: 230px; } &_position_center { @@ -46,7 +43,7 @@ grid-area: image; justify-self: end; - margin-right: 60px; + margin-right: var(--g-spacing-10); color: var(--g-color-base-info-light-hover); @@ -56,9 +53,6 @@ } &__title { - align-self: center; - grid-area: title; - font-weight: 500; &_size_s { @@ -71,16 +65,11 @@ } &__description { - grid-area: description; - @include mixins.body-2-typography(); } - &__actions { - grid-area: actions; - - & > * { - margin-right: 8px; - } + &__content { + align-self: center; + grid-area: content; } } diff --git a/src/components/EmptyState/EmptyState.tsx b/src/components/EmptyState/EmptyState.tsx index cdb3afa627..a56e2741b1 100644 --- a/src/components/EmptyState/EmptyState.tsx +++ b/src/components/EmptyState/EmptyState.tsx @@ -1,4 +1,4 @@ -import {Icon} from '@gravity-ui/uikit'; +import {Flex, Icon, Text} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; @@ -8,20 +8,22 @@ import './EmptyState.scss'; const block = cn('empty-state'); -const sizes = { +export const EMPTY_STATE_SIZES = { xs: 100, s: 150, - m: 250, + m: 230, l: 350, }; export interface EmptyStateProps { - title: string; + title: React.ReactNode; image?: React.ReactNode; description?: React.ReactNode; actions?: React.ReactNode[]; - size?: keyof typeof sizes; + size?: keyof typeof EMPTY_STATE_SIZES; position?: 'left' | 'center'; + pageTitle?: string; + className?: string; } export const EmptyState = ({ @@ -31,21 +33,33 @@ export const EmptyState = ({ actions, size = 'm', position = 'center', + pageTitle, + className, }: EmptyStateProps) => { return ( -
+
+ {pageTitle ? {pageTitle} : null}
{image ? ( image ) : ( - + )}
- -
{title}
-
{description}
-
{actions}
+ + +
{title}
+ {description ? ( +
{description}
+ ) : null} +
+ {actions ? {actions} : null} +
); diff --git a/src/components/Errors/403/AccessDenied.tsx b/src/components/Errors/403/AccessDenied.tsx index 53555cc26f..932dda30de 100644 --- a/src/components/Errors/403/AccessDenied.tsx +++ b/src/components/Errors/403/AccessDenied.tsx @@ -1,17 +1,22 @@ -import {EmptyState} from '../../EmptyState'; +import {EMPTY_STATE_SIZES, EmptyState} from '../../EmptyState'; import type {EmptyStateProps} from '../../EmptyState'; import {Illustration} from '../../Illustration'; import i18n from '../i18n'; -interface AccessDeniedProps extends Omit { - title?: string; - description?: string; +interface AccessDeniedProps extends Omit { + title?: React.ReactNode; } -export const AccessDenied = ({title, description, ...restProps}: AccessDeniedProps) => { +export const AccessDenied = ({ + title, + description, + image, + size = 'm', + ...restProps +}: AccessDeniedProps) => { return ( } + image={image || } title={title || i18n('403.title')} description={description || i18n('403.description')} {...restProps} diff --git a/src/components/Errors/PageError/PageError.scss b/src/components/Errors/PageError/PageError.scss new file mode 100644 index 0000000000..fee87a161d --- /dev/null +++ b/src/components/Errors/PageError/PageError.scss @@ -0,0 +1,7 @@ +.ydb-page-error { + display: grid; + align-items: center; + grid-template-rows: min-content auto; + + height: 100%; +} diff --git a/src/components/Errors/PageError/PageError.tsx b/src/components/Errors/PageError/PageError.tsx index 75bea83c2c..9d35166f7b 100644 --- a/src/components/Errors/PageError/PageError.tsx +++ b/src/components/Errors/PageError/PageError.tsx @@ -1,36 +1,59 @@ import React from 'react'; +import {cn} from '../../../utils/cn'; import {isAccessError, isRedirectToAuth} from '../../../utils/response'; import type {EmptyStateProps} from '../../EmptyState'; -import {EmptyState} from '../../EmptyState'; +import {EMPTY_STATE_SIZES, EmptyState} from '../../EmptyState'; import {Illustration} from '../../Illustration'; import {AccessDenied} from '../403'; import {ResponseError} from '../ResponseError'; import i18n from '../i18n'; -interface PageErrorProps extends Omit { - title?: string; - description?: string; +import './PageError.scss'; + +const b = cn('ydb-page-error'); + +interface PageErrorProps extends Omit { + title?: React.ReactNode; error: unknown; children?: React.ReactNode; + errorPageTitle?: string; } -export function PageError({title, description, error, children, ...restProps}: PageErrorProps) { +export function PageError({ + title, + description, + error, + children, + size = 'm', + errorPageTitle, + ...restProps +}: PageErrorProps) { if (isRedirectToAuth(error)) { // Do not show an error, because we redirect to auth anyway. return null; } if (isAccessError(error)) { - return ; + return ( + + ); } if (error || description) { return ( } + image={} title={title || i18n('error.title')} description={error ? : description} + pageTitle={errorPageTitle} + className={b()} {...restProps} /> ); diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index e97965c0f7..15f6f1fe34 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -20,6 +20,7 @@ import { useMetaCapabilitiesQuery, } from '../../store/reducers/capabilities/hooks'; import {nodesListApi} from '../../store/reducers/nodesList'; +import {uiFactory} from '../../uiFactory/uiFactory'; import {cn} from '../../utils/cn'; import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import {lazyComponent} from '../../utils/lazyComponent'; @@ -28,6 +29,7 @@ import Authentication from '../Authentication/Authentication'; import {getClusterPath} from '../Cluster/utils'; import Header from '../Header/Header'; +import {useAppTitle} from './AppTitleContext'; import { ClusterSlot, ClustersSlot, @@ -192,10 +194,17 @@ function DataWrapper({children}: {children: React.ReactNode}) { function GetUser({children}: {children: React.ReactNode}) { const database = useDatabaseFromQuery(); const {isLoading, error} = authenticationApi.useWhoamiQuery({database}); + const {appTitle} = useAppTitle(); return ( - {children} + + {children} + ); } diff --git a/src/containers/Clusters/Clusters.tsx b/src/containers/Clusters/Clusters.tsx index 2cadf127de..44552628e7 100644 --- a/src/containers/Clusters/Clusters.tsx +++ b/src/containers/Clusters/Clusters.tsx @@ -6,6 +6,7 @@ import {Flex, Icon, Select, TableColumnSetup, Text} from '@gravity-ui/uikit'; import {Helmet} from 'react-helmet-async'; import {AutoRefreshControl} from '../../components/AutoRefreshControl/AutoRefreshControl'; +import {PageError} from '../../components/Errors/PageError/PageError'; import {ResponseError} from '../../components/Errors/ResponseError'; import {Loader} from '../../components/Loader'; import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable'; @@ -27,6 +28,7 @@ import {uiFactory} from '../../uiFactory/uiFactory'; import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {useSelectedColumns} from '../../utils/hooks/useSelectedColumns'; +import {isAccessError} from '../../utils/response'; import {getMinorVersion} from '../../utils/versions'; import {CLUSTERS_COLUMNS_WIDTH_LS_KEY, getClustersColumns} from './columns'; @@ -136,97 +138,104 @@ export function Clusters() { ); }; + const showBlockingError = isAccessError(query.error); + return ( -
- - {i18n('page_title')} - - - {renderPageTitle()} - - -
- } - onChange={changeClusterName} - value={clusterName} - /> -
-
- -
-
- +
+
+ +
+
+ +
+
+ {clusters?.length ? ( + + {i18n('clusters-count', {count: filteredClusters?.length})} + + ) : null} + {query.isError ? : null} + {query.isLoading ? : null} + {query.fulfilledTimeStamp ? ( +
+
+ +
+
+ ) : null} +
+ ); } diff --git a/src/containers/Header/Header.tsx b/src/containers/Header/Header.tsx index 1a2fd2f620..6ec700328c 100644 --- a/src/containers/Header/Header.tsx +++ b/src/containers/Header/Header.tsx @@ -22,6 +22,7 @@ import { useEditDatabaseFeatureAvailable, } from '../../store/reducers/capabilities/hooks'; import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster'; +import {clustersApi} from '../../store/reducers/clusters/clusters'; import {tenantApi} from '../../store/reducers/tenant/tenant'; import {uiFactory} from '../../uiFactory/uiFactory'; import {cn} from '../../utils/cn'; @@ -36,6 +37,7 @@ import { useIsUserAllowedToMakeChanges, useIsViewerUser, } from '../../utils/hooks/useIsUserAllowedToMakeChanges'; +import {isAccessError} from '../../utils/response'; import {getClusterPath} from '../Cluster/utils'; import {getBreadcrumbs} from './breadcrumbs'; @@ -65,8 +67,16 @@ function Header() { const isDatabasePage = location.pathname === '/tenant'; const isClustersPage = location.pathname === '/clusters'; + const {isFetching: isClustersFetching, error: clustersError} = + clustersApi.useGetClustersListQuery(undefined, { + skip: !isClustersPage, + }); + const isAddClusterAvailable = - useAddClusterFeatureAvailable() && uiFactory.onAddCluster !== undefined; + useAddClusterFeatureAvailable() && + uiFactory.onAddCluster !== undefined && + !isClustersFetching && + !isAccessError(clustersError); const isEditDBAvailable = useEditDatabaseFeatureAvailable() && uiFactory.onEditDB !== undefined; const isDeleteDBAvailable = diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx index 68813d4752..83afe7f437 100644 --- a/src/containers/Tenant/Tenant.tsx +++ b/src/containers/Tenant/Tenant.tsx @@ -10,6 +10,7 @@ import {overviewApi} from '../../store/reducers/overview/overview'; import {selectSchemaObjectData} from '../../store/reducers/schema/schema'; import {useTenantBaseInfo} from '../../store/reducers/tenant/tenant'; import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../types/additionalProps'; +import {uiFactory} from '../../uiFactory/uiFactory'; import {cn} from '../../utils/cn'; import {DEFAULT_IS_TENANT_SUMMARY_COLLAPSED, DEFAULT_SIZE_TENANT_KEY} from '../../utils/constants'; import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; @@ -130,7 +131,10 @@ export function Tenant(props: TenantProps) { titleTemplate={`%s — ${title} — ${appTitle}`} /> - + { renderBackups?: RenderBackups; renderEvents?: RenderEvents; + clusterOrDatabaseAccessError?: Partial; healthcheck: { getHealthckechViewTitles: GetHealthcheckViewTitles;