diff --git a/src/components/Errors/PageError/PageError.tsx b/src/components/Errors/PageError/PageError.tsx index 679e279e8..17a03f35a 100644 --- a/src/components/Errors/PageError/PageError.tsx +++ b/src/components/Errors/PageError/PageError.tsx @@ -34,5 +34,10 @@ export function PageError({title, description, error, children, ...restProps}: P } export function isAccessError(error: unknown) { - return Boolean(error && typeof error === 'object' && 'status' in error && error.status === 403); + return Boolean( + error && + typeof error === 'object' && + 'status' in error && + (error.status === 403 || error.status === 401), + ); } diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 9b783b0e3..0d5e4dd75 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -43,6 +43,7 @@ type RouteSlot = { path: string; slot: SlotComponent; component: React.ComponentType; + wrapper?: React.ComponentType; exact?: boolean; }; const routesSlots: RouteSlot[] = [ @@ -50,31 +51,37 @@ const routesSlots: RouteSlot[] = [ path: routes.cluster, slot: ClusterSlot, component: lazyComponent(() => import('../Cluster/Cluster'), 'Cluster'), + wrapper: DataWrapper, }, { path: routes.tenant, slot: TenantSlot, component: lazyComponent(() => import('../Tenant/Tenant'), 'Tenant'), + wrapper: DataWrapper, }, { path: routes.node, slot: NodeSlot, component: lazyComponent(() => import('../Node/Node'), 'Node'), + wrapper: DataWrapper, }, { path: routes.pDisk, slot: PDiskPageSlot, component: lazyComponent(() => import('../PDiskPage/PDiskPage'), 'PDiskPage'), + wrapper: DataWrapper, }, { path: routes.vDisk, slot: VDiskPageSlot, component: lazyComponent(() => import('../VDiskPage/VDiskPage'), 'VDiskPage'), + wrapper: DataWrapper, }, { path: routes.tablet, slot: TabletSlot, component: lazyComponent(() => import('../Tablet'), 'Tablet'), + wrapper: DataWrapper, }, { path: routes.tabletsFilters, @@ -83,6 +90,7 @@ const routesSlots: RouteSlot[] = [ () => import('../TabletsFilters/TabletsFilters'), 'TabletsFilters', ), + wrapper: DataWrapper, }, ]; @@ -105,7 +113,12 @@ function renderRouteSlot(slots: SlotMap, route: RouteSlot) { } else { content = slot.rendered; } - return
{content}
; + const Wrapper = route.wrapper ?? React.Fragment; + return ( +
+ {content} +
+ ); }} /> ); @@ -144,29 +157,33 @@ export function Content(props: ContentProps) { {additionalRoutes?.rendered} {/* Single cluster routes */} - - - -
- - {routesSlots.map((route) => { - return renderRouteSlot(slots, route); - })} - ( - - )} - /> - - +
+ + {routesSlots.map((route) => { + return renderRouteSlot(slots, route); + })} + } + /> + ); } +function DataWrapper({children}: {children: React.ReactNode}) { + return ( + + + + {children} + + ); +} + function GetUser({children}: {children: React.ReactNode}) { const {isLoading, error} = authenticationApi.useWhoamiQuery(undefined); diff --git a/src/containers/Tenant/Acl/Acl.scss b/src/containers/Tenant/Acl/Acl.scss index 4694799e4..710cce076 100644 --- a/src/containers/Tenant/Acl/Acl.scss +++ b/src/containers/Tenant/Acl/Acl.scss @@ -5,10 +5,15 @@ &__result { padding-bottom: var(--g-spacing-4); padding-left: var(--g-spacing-2); + + &_no-title { + margin-top: var(--g-spacing-3); + } } &__definition-content { display: flex; flex-direction: column; + align-items: flex-end; } &__list-title { margin: var(--g-spacing-3) 0 var(--g-spacing-5); diff --git a/src/containers/Tenant/Acl/Acl.tsx b/src/containers/Tenant/Acl/Acl.tsx index 349072ef2..9d85d9a32 100644 --- a/src/containers/Tenant/Acl/Acl.tsx +++ b/src/containers/Tenant/Acl/Acl.tsx @@ -2,6 +2,8 @@ import React from 'react'; import {DefinitionList} from '@gravity-ui/components'; import type {DefinitionListItem} from '@gravity-ui/components'; +import {SquareCheck} from '@gravity-ui/icons'; +import {Icon} from '@gravity-ui/uikit'; import {ResponseError} from '../../../components/Errors/ResponseError'; import {Loader} from '../../../components/Loader'; @@ -94,6 +96,7 @@ function getAclListItems(acl?: TACE[]): DefinitionListItem[] { return { name: Subject, content: , + multilineName: true, }; } return { @@ -105,6 +108,7 @@ function getAclListItems(acl?: TACE[]): DefinitionListItem[] { return { name: aclParamToName[key], content: , + multilineName: true, }; } return undefined; @@ -121,8 +125,22 @@ function getOwnerItem(owner?: string): DefinitionListItem[] { } return [ { - name: {preparedOwner}, - content: {i18n('title_owner')}, + name: preparedOwner, + content: i18n('title_owner'), + multilineName: true, + }, + ]; +} + +function getInterruptInheritanceItem(flag?: boolean): DefinitionListItem[] { + if (!flag) { + return []; + } + return [ + { + name: i18n('title_interupt-inheritance'), + content: , + multilineName: true, }, ]; } @@ -132,13 +150,15 @@ export const Acl = ({path, database}: {path: string; database: string}) => { const loading = isFetching && !currentData; - const {acl, effectiveAcl, owner} = currentData || {}; + const {acl, effectiveAcl, owner, interruptInheritance} = currentData || {}; const aclListItems = getAclListItems(acl); const effectiveAclListItems = getAclListItems(effectiveAcl); const ownerItem = getOwnerItem(owner); + const interruptInheritanceItem = getInterruptInheritanceItem(interruptInheritance); + if (loading) { return ; } @@ -155,26 +175,34 @@ export const Acl = ({path, database}: {path: string; database: string}) => { return (
- {accessRightsItems.length ? ( - -
{i18n('title_rights')}
- -
- ) : null} - {effectiveAclListItems.length ? ( - -
{i18n('title_effective-rights')}
- -
- ) : null} + + +
); }; + +interface AclDefinitionListProps { + items: DefinitionListItem[]; + title?: string; +} + +function AclDefinitionList({items, title}: AclDefinitionListProps) { + if (!items.length) { + return null; + } + return ( + + {title &&
{title}
} + +
+ ); +} diff --git a/src/containers/Tenant/Acl/i18n/en.json b/src/containers/Tenant/Acl/i18n/en.json index 0bf6aa879..f9c3ff902 100644 --- a/src/containers/Tenant/Acl/i18n/en.json +++ b/src/containers/Tenant/Acl/i18n/en.json @@ -2,5 +2,6 @@ "title_rights": "Access Rights", "title_effective-rights": "Effective Access Rights", "title_owner": "Owner", + "title_interupt-inheritance": "Interrupt inheritance", "description_empty": "No Acl data" } diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index 4b5634126..77d81abb4 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -1,8 +1,7 @@ import React from 'react'; import {HelpPopover} from '@gravity-ui/components'; -import {LayoutHeaderCellsLargeFill} from '@gravity-ui/icons'; -import {Button, Icon, Tabs} from '@gravity-ui/uikit'; +import {Tabs} from '@gravity-ui/uikit'; import qs from 'qs'; import {Link, useLocation} from 'react-router-dom'; import {StringParam, useQueryParam} from 'use-query-params'; @@ -16,13 +15,9 @@ import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import {Loader} from '../../../components/Loader'; import SplitPane from '../../../components/SplitPane'; import routes, {createExternalUILink, createHref} from '../../../routes'; -import {setShowPreview, useGetSchemaQuery} from '../../../store/reducers/schema/schema'; -import { - TENANT_PAGES_IDS, - TENANT_QUERY_TABS_ID, - TENANT_SUMMARY_TABS_IDS, -} from '../../../store/reducers/tenant/constants'; -import {setQueryTab, setSummaryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; +import {useGetSchemaQuery} from '../../../store/reducers/schema/schema'; +import {TENANT_SUMMARY_TABS_IDS} from '../../../store/reducers/tenant/constants'; +import {setSummaryTab} from '../../../store/reducers/tenant/tenant'; import {EPathSubType, EPathType} from '../../../types/api/schema'; import {cn} from '../../../utils/cn'; import { @@ -41,6 +36,7 @@ import {SchemaTree} from '../Schema/SchemaTree/SchemaTree'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; import i18n from '../i18n'; +import {getSummaryControls} from '../utils/controls'; import { PaneVisibilityActionTypes, PaneVisibilityToggleButtons, @@ -84,6 +80,7 @@ export function ObjectSummary({ isCollapsed, }: ObjectSummaryProps) { const dispatch = useTypedDispatch(); + const [, setCurrentPath] = useQueryParam('schema', StringParam); const [commonInfoVisibilityState, dispatchCommonInfoVisibilityState] = React.useReducer( paneVisibilityToggleReducerCreator(DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED), undefined, @@ -326,25 +323,16 @@ export function ObjectSummary({ dispatchCommonInfoVisibilityState(PaneVisibilityActionTypes.clear); }; - const onOpenPreview = () => { - dispatch(setShowPreview(true)); - dispatch(setTenantPage(TENANT_PAGES_IDS.query)); - dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); - }; - const renderCommonInfoControls = () => { const showPreview = isTableType(type) && !isIndexTableType(subType); return ( - {showPreview && ( - - )} + {showPreview && + getSummaryControls( + dispatch, + {setActivePath: setCurrentPath}, + 'm', + )(path, 'preview')} { const query = `--!syntax_v1\nselect * from \`${path}\` limit 32`; const {currentData, isFetching, error} = previewApi.useSendQueryQuery( {database, query, action: isExternalTableType(type) ? 'execute-query' : 'execute-scan'}, - {pollingInterval: autoRefreshInterval, skip: !isPreviewAvailable}, + { + pollingInterval: autoRefreshInterval, + skip: !isPreviewAvailable, + refetchOnMountOrArgChange: true, + }, ); const loading = isFetching && currentData === undefined; const data = currentData ?? {}; diff --git a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx index c845815e0..3da78debe 100644 --- a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +++ b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx @@ -9,9 +9,9 @@ import {useCreateDirectoryFeatureAvailable} from '../../../../store/reducers/cap import {schemaApi} from '../../../../store/reducers/schema/schema'; import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema'; import {useQueryExecutionSettings, useTypedDispatch} from '../../../../utils/hooks'; +import {getSchemaControls} from '../../utils/controls'; import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema'; import {getActions} from '../../utils/schemaActions'; -import {getControls} from '../../utils/schemaControls'; import {CreateDirectoryDialog} from '../CreateDirectoryDialog/CreateDirectoryDialog'; interface SchemaTreeProps { @@ -118,7 +118,7 @@ export function SchemaTree(props: SchemaTreeProps) { ? handleOpenCreateDirectoryDialog : undefined, })} - renderAdditionalNodeElements={getControls(dispatch, { + renderAdditionalNodeElements={getSchemaControls(dispatch, { setActivePath: onActivePathUpdate, })} activePath={currentPath} diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx index 1ac702afb..b59e8d69a 100644 --- a/src/containers/Tenant/Tenant.tsx +++ b/src/containers/Tenant/Tenant.tsx @@ -4,6 +4,7 @@ import {Helmet} from 'react-helmet-async'; import {StringParam, useQueryParams} from 'use-query-params'; import {PageError, isAccessError} from '../../components/Errors/PageError/PageError'; +import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import SplitPane from '../../components/SplitPane'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {useGetSchemaQuery} from '../../store/reducers/schema/schema'; @@ -73,7 +74,7 @@ export function Tenant(props: TenantProps) { const path = schema ?? tenantName; - const {data: currentItem, error} = useGetSchemaQuery({path, database: tenantName}); + const {data: currentItem, error, isLoading} = useGetSchemaQuery({path, database: tenantName}); const {PathType: currentPathType, PathSubType: currentPathSubType} = currentItem?.PathDescription?.Self || {}; @@ -97,35 +98,37 @@ export function Tenant(props: TenantProps) { defaultTitle={`${title} — YDB Monitoring`} titleTemplate={`%s — ${title} — YDB Monitoring`} /> - - - -
- + + + -
-
-
+
+ +
+ + + ); } diff --git a/src/containers/Tenant/utils/schemaControls.tsx b/src/containers/Tenant/utils/controls.tsx similarity index 58% rename from src/containers/Tenant/utils/schemaControls.tsx rename to src/containers/Tenant/utils/controls.tsx index 1b3a616d5..500d96033 100644 --- a/src/containers/Tenant/utils/schemaControls.tsx +++ b/src/containers/Tenant/utils/controls.tsx @@ -1,7 +1,9 @@ import {LayoutHeaderCellsLargeFill} from '@gravity-ui/icons'; +import type {ButtonSize} from '@gravity-ui/uikit'; import {Button, Icon} from '@gravity-ui/uikit'; import type {NavigationTreeNodeType, NavigationTreeProps} from 'ydb-ui-components'; +import {api} from '../../../store/reducers/api'; import {setShowPreview} from '../../../store/reducers/schema/schema'; import {TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants'; import {setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; @@ -20,6 +22,7 @@ const bindActions = ( return { openPreview: () => { + dispatch(api.util.invalidateTags(['PreviewData'])); dispatch(setShowPreview(true)); dispatch(setTenantPage(TENANT_PAGES_IDS.query)); dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); @@ -30,20 +33,30 @@ const bindActions = ( type Controls = ReturnType['renderAdditionalNodeElements']>; -export const getControls = - (dispatch: React.Dispatch, additionalEffects: ControlsAdditionalEffects) => +type SummaryType = 'preview'; + +const getPreviewControl = (options: ReturnType, size?: ButtonSize) => { + return ( + + ); +}; + +export const getSchemaControls = + ( + dispatch: React.Dispatch, + additionalEffects: ControlsAdditionalEffects, + size?: ButtonSize, + ) => (path: string, type: NavigationTreeNodeType) => { const options = bindActions(path, dispatch, additionalEffects); - const openPreview = ( - - ); + const openPreview = getPreviewControl(options, size); const nodeTypeToControls: Record = { async_replication: undefined, @@ -68,3 +81,18 @@ export const getControls = return nodeTypeToControls[type]; }; + +export const getSummaryControls = + ( + dispatch: React.Dispatch, + additionalEffects: ControlsAdditionalEffects, + size?: ButtonSize, + ) => + (path: string, type: SummaryType) => { + const options = bindActions(path, dispatch, additionalEffects); + const openPreview = getPreviewControl(options, size); + + const summaryControls: Record = {preview: openPreview}; + + return summaryControls[type]; + }; diff --git a/src/store/reducers/api.ts b/src/store/reducers/api.ts index fa2760608..e3ff5fd7f 100644 --- a/src/store/reducers/api.ts +++ b/src/store/reducers/api.ts @@ -9,7 +9,7 @@ export const api = createApi({ */ endpoints: () => ({}), invalidationBehavior: 'immediately', - tagTypes: ['All', 'PDiskData', 'UserData'], + tagTypes: ['All', 'PDiskData', 'UserData', 'PreviewData'], }); export const _NEVER = Symbol(); diff --git a/src/store/reducers/preview.ts b/src/store/reducers/preview.ts index e913c2e21..76b72c4b0 100644 --- a/src/store/reducers/preview.ts +++ b/src/store/reducers/preview.ts @@ -28,7 +28,7 @@ export const previewApi = api.injectEndpoints({ return {error: error || new Error('Unauthorized')}; } }, - providesTags: ['All'], + providesTags: ['All', 'PreviewData'], }), }), overrideExisting: 'throw', diff --git a/src/store/reducers/schemaAcl/schemaAcl.ts b/src/store/reducers/schemaAcl/schemaAcl.ts index fbdfc4f10..949f0f802 100644 --- a/src/store/reducers/schemaAcl/schemaAcl.ts +++ b/src/store/reducers/schemaAcl/schemaAcl.ts @@ -11,6 +11,7 @@ export const schemaAclApi = api.injectEndpoints({ acl: data.Common.ACL, effectiveAcl: data.Common.EffectiveACL, owner: data.Common.Owner, + interruptInheritance: data.Common.InterruptInheritance, }, }; } catch (error) { diff --git a/src/types/api/acl.ts b/src/types/api/acl.ts index 3febe43c0..b9ce22079 100644 --- a/src/types/api/acl.ts +++ b/src/types/api/acl.ts @@ -15,6 +15,7 @@ export interface TMetaCommonInfo { Owner?: string; ACL?: TACE[]; EffectiveACL?: TACE[]; + InterruptInheritance?: boolean; } export interface TACE {