From 790391d046ee1a55406687a405a5dc663375a6e5 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 17 Aug 2024 10:55:53 +0300 Subject: [PATCH 1/5] feat(Tablet): redesign tablet page, add data from hive --- src/components/EmptyState/EmptyState.tsx | 17 ++ .../InfoViewerSkeleton/InfoViewerSkeleton.tsx | 28 +- src/components/PDiskInfo/PDiskInfo.tsx | 2 +- src/components/Tablet/Tablet.tsx | 5 +- src/containers/Tablet/Tablet.scss | 82 +----- src/containers/Tablet/Tablet.tsx | 245 ++++++++++++------ .../Tablet/TabletInfo/TabletInfo.tsx | 80 ------ .../TabletControls/TabletControls.tsx | 37 +-- .../{ => components}/TabletControls/index.ts | 0 .../components/TabletInfo/TabletInfo.tsx | 129 +++++++++ .../Tablet/components/TabletInfo/i18n/en.json | 12 + .../components/TabletInfo/i18n/index.ts | 7 + .../{ => components}/TabletInfo/index.ts | 0 .../TabletStorageInfo/TabletStorageInfo.scss | 71 +++++ .../TabletStorageInfo/TabletStorageInfo.tsx | 51 ++++ .../components/TabletStorageInfo/columns.tsx | 105 ++++++++ .../components/TabletStorageInfo/i18n/en.json | 7 + .../TabletStorageInfo/i18n/index.ts | 7 + .../components/TabletStorageInfo/shared.ts | 3 + .../components/TabletStorageInfo/types.ts | 6 + .../components/TabletStorageInfo/utils.ts | 36 +++ .../TabletTable/TabletTable.tsx | 12 +- .../{ => components}/TabletTable/index.ts | 0 src/containers/Tablet/i18n/en.json | 8 +- src/containers/Tablet/shared.ts | 3 + src/containers/Tablet/utils.ts | 3 + src/services/api.ts | 29 ++- src/store/reducers/tablet.ts | 43 ++- src/types/api/tablet.ts | 23 ++ src/types/store/tablets.ts | 1 + src/utils/dataFormatters/dataFormatters.ts | 5 + src/utils/developerUI/developerUI.ts | 10 +- 32 files changed, 776 insertions(+), 291 deletions(-) delete mode 100644 src/containers/Tablet/TabletInfo/TabletInfo.tsx rename src/containers/Tablet/{ => components}/TabletControls/TabletControls.tsx (74%) rename src/containers/Tablet/{ => components}/TabletControls/index.ts (100%) create mode 100644 src/containers/Tablet/components/TabletInfo/TabletInfo.tsx create mode 100644 src/containers/Tablet/components/TabletInfo/i18n/en.json create mode 100644 src/containers/Tablet/components/TabletInfo/i18n/index.ts rename src/containers/Tablet/{ => components}/TabletInfo/index.ts (100%) create mode 100644 src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss create mode 100644 src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx create mode 100644 src/containers/Tablet/components/TabletStorageInfo/columns.tsx create mode 100644 src/containers/Tablet/components/TabletStorageInfo/i18n/en.json create mode 100644 src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts create mode 100644 src/containers/Tablet/components/TabletStorageInfo/shared.ts create mode 100644 src/containers/Tablet/components/TabletStorageInfo/types.ts create mode 100644 src/containers/Tablet/components/TabletStorageInfo/utils.ts rename src/containers/Tablet/{ => components}/TabletTable/TabletTable.tsx (79%) rename src/containers/Tablet/{ => components}/TabletTable/index.ts (100%) create mode 100644 src/containers/Tablet/shared.ts create mode 100644 src/containers/Tablet/utils.ts diff --git a/src/components/EmptyState/EmptyState.tsx b/src/components/EmptyState/EmptyState.tsx index b7d44d3fa..f04f03f0c 100644 --- a/src/components/EmptyState/EmptyState.tsx +++ b/src/components/EmptyState/EmptyState.tsx @@ -49,3 +49,20 @@ export const EmptyState = ({ ); }; + +interface EmptyStateWrapperProps extends EmptyStateProps { + isEmpty?: boolean; + children: React.ReactNode; + className?: string; +} + +export function EmptyStateWrapper({isEmpty, children, className, ...rest}: EmptyStateWrapperProps) { + if (isEmpty) { + return ( +
+ +
+ ); + } + return children; +} diff --git a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx index eaae8966c..f620eb4d5 100644 --- a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx +++ b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx @@ -1,6 +1,7 @@ import {Skeleton} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; +import {useDelayed} from '../../utils/hooks/useDelayed'; import './InfoViewerSkeleton.scss'; @@ -16,15 +17,22 @@ const SkeletonLabel = () => ( interface InfoViewerSkeletonProps { className?: string; rows?: number; + delay?: number; } -export const InfoViewerSkeleton = ({rows = 8, className}: InfoViewerSkeletonProps) => ( -
- {[...new Array(rows)].map((_, index) => ( -
- - -
- ))} -
-); +export const InfoViewerSkeleton = ({rows = 8, className, delay = 600}: InfoViewerSkeletonProps) => { + const show = useDelayed(delay); + if (!show) { + return null; + } + return ( +
+ {[...new Array(rows)].map((_, index) => ( +
+ + +
+ ))} +
+ ); +}; diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx index 65b1ccc21..918988136 100644 --- a/src/components/PDiskInfo/PDiskInfo.tsx +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -164,7 +164,7 @@ function getPDiskInfo({ label: pDiskInfoKeyset('links'), value: ( - {withPDiskPageLink && ( + {isUserAllowedToMakeChanges && ( { const {TabletId: id, NodeId, Type} = tablet; const status = tablet.Overall?.toLowerCase(); - const tabletPath = - id && createHref(routes.tablet, {id}, {nodeId: NodeId, tenantName, type: Type}); + const tabletPath = id && getTabletPagePath(id, {nodeId: NodeId, tenantName, type: Type}); return ( * { - margin: 0 10px 0 0; - } - } - - &__top-label { - margin-right: 16px; - - font-weight: 500; - text-transform: uppercase; - - @include body-2-typography(); + &__section-title { + margin: var(--g-spacing-1) 0 var(--g-spacing-3); + @include text-subheader-2(); } } diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx index fdcdf0e29..5b837d716 100644 --- a/src/containers/Tablet/Tablet.tsx +++ b/src/containers/Tablet/Tablet.tsx @@ -1,42 +1,72 @@ import React from 'react'; -import {ArrowUpRightFromSquare} from '@gravity-ui/icons'; -import {Link as ExternalLink, Icon} from '@gravity-ui/uikit'; +import {Flex, Spin, Tabs} from '@gravity-ui/uikit'; import {skipToken} from '@reduxjs/toolkit/query'; import {Helmet} from 'react-helmet-async'; import {useLocation, useParams} from 'react-router-dom'; +import {StringParam, useQueryParams} from 'use-query-params'; +import {z} from 'zod'; -import {EmptyState} from '../../components/EmptyState'; -import {EntityStatus} from '../../components/EntityStatus/EntityStatus'; +import {EmptyStateWrapper} from '../../components/EmptyState'; +import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle'; import {ResponseError} from '../../components/Errors/ResponseError'; -import {Loader} from '../../components/Loader'; -import {Tag} from '../../components/Tag'; -import {parseQuery} from '../../routes'; -import {backend} from '../../store'; +import {InternalLink} from '../../components/InternalLink'; +import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; +import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; +import {getTabletPagePath, parseQuery} from '../../routes'; +import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {tabletApi} from '../../store/reducers/tablet'; +import {EFlag} from '../../types/api/enums'; import type {EType} from '../../types/api/tablet'; -import {cn} from '../../utils/cn'; -import {CLUSTER_DEFAULT_TITLE, DEVELOPER_UI_TITLE} from '../../utils/constants'; -import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks'; +import {CLUSTER_DEFAULT_TITLE} from '../../utils/constants'; +import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; -import {TabletControls} from './TabletControls'; -import {TabletInfo} from './TabletInfo'; -import {TabletTable} from './TabletTable'; +import {TabletControls} from './components/TabletControls'; +import {TabletInfo} from './components/TabletInfo'; +import {TabletStorageInfo} from './components/TabletStorageInfo/TabletStorageInfo'; +import {TabletTable} from './components/TabletTable'; import i18n from './i18n'; +import {b} from './shared'; +import {hasHive} from './utils'; import './Tablet.scss'; -export const b = cn('tablet-page'); +const TABLET_TABS_IDS = { + history: 'history', + channels: 'channels', +} as const; + +const TABLET_PAGE_TABS = [ + { + id: TABLET_TABS_IDS.history, + get title() { + return i18n('label_tablet-history'); + }, + }, + { + id: TABLET_TABS_IDS.channels, + get title() { + return i18n('label_tablet-channels'); + }, + isAdvanced: true, + }, +]; + +const tabletTabSchema = z.nativeEnum(TABLET_TABS_IDS).catch(TABLET_TABS_IDS.history); export const Tablet = () => { - const isFirstDataFetchRef = React.useRef(true); - const dispatch = useTypedDispatch(); const location = useLocation(); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); + + const {id} = useParams<{id: string}>(); + + const [{activeTab}, setParams] = useQueryParams({ + activeTab: StringParam, + }); - const params = useParams<{id: string}>(); - const {id} = params; + const tabletTab = tabletTabSchema.parse(activeTab); const { nodeId: queryNodeId, @@ -47,7 +77,7 @@ export const Tablet = () => { const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isFetching, error, refetch} = tabletApi.useGetTabletQuery( - {id}, + {id, database: queryTenantName?.toString()}, {pollingInterval: autoRefreshInterval}, ); @@ -58,8 +88,35 @@ export const Tablet = () => { tablet.TenantId ? {tenantId: tablet.TenantId} : skipToken, ); + const hasHiveId = hasHive(tablet.HiveId); + + const noAdvancedInfo = !isUserAllowedToMakeChanges || !hasHiveId; + + React.useEffect(() => { + if (noAdvancedInfo && TABLET_PAGE_TABS.find(({id}) => id === tabletTab)?.isAdvanced) { + setParams({activeTab: TABLET_TABS_IDS.history}); + } + }, [noAdvancedInfo, tabletTab, setParams]); + + const { + currentData: advancedData, + refetch: refetchAdvancedInfo, + isFetching: isFetchingAdvancedData, + } = tabletApi.useGetAdvancedTableInfoQuery( + {id, hiveId: tablet.HiveId}, + { + pollingInterval: autoRefreshInterval, + skip: noAdvancedInfo || activeTab !== 'channels', + }, + ); + + const refetchTabletInfo = React.useCallback(async () => { + await Promise.all([refetch(), refetchAdvancedInfo()]); + }, [refetch, refetchAdvancedInfo]); + const nodeId = tablet.NodeId?.toString() || queryNodeId?.toString(); const tenantName = tenantPath || queryTenantName?.toString(); + const type = tablet.Type || (queryTabletType?.toString() as EType | undefined); React.useEffect(() => { @@ -73,80 +130,108 @@ export const Tablet = () => { ); }, [dispatch, tenantName, id, nodeId, type]); - const renderExternalLinks = (link: {name: string; path: string}, index: number) => { + const renderPageMeta = () => { + const {Leader, Type} = tablet; + const database = tenantName ? `${i18n('tablet.meta-database')}: ${tenantName}` : undefined; + const type = Type ? Type : undefined; + const follower = Leader === false ? i18n('tablet.meta-follower').toUpperCase() : undefined; + + return ; + }; + + const renderHelmet = () => { return ( -
  • - - {link.name} - -
  • + + {`${id} — ${i18n('tablet.header')} — ${ + tenantName || queryClusterName || CLUSTER_DEFAULT_TITLE + }`} + ); }; - const renderView = () => { - if (loading && id !== tabletId && isFirstDataFetchRef.current) { - return ; + const renderTabs = () => { + return ( + //block wrapper for tabs +
    + + isAdvanced ? !noAdvancedInfo : true, + )} + activeTab={tabletTab} + wrapTo={({id}, tabNode) => { + const path = tabletId + ? getTabletPagePath(tabletId, {activeTab: id}) + : undefined; + return ( + + {tabNode} + + ); + }} + /> +
    + ); + }; + + const renderTabsContent = () => { + switch (tabletTab) { + case 'channels': { + return ( + + + + ); + } + case 'history': { + return ; + } + default: + return null; } + }; + const renderView = () => { if (error && !currentData) { return ; } - if (!tablet || !Object.keys(tablet).length) { - return ( -
    - -
    - ); - } - - const {TabletId, Overall, Leader} = tablet; - - const externalLinks = [ - { - name: `${DEVELOPER_UI_TITLE} - tablet`, - path: `/tablets?TabletID=${TabletId}`, - }, - ]; - + const {TabletId, Overall} = tablet; return ( -
    + {error ? : null} -
    -
    -
      {externalLinks.map(renderExternalLinks)}
    -
    - {i18n('tablet.header')} - - - - - {Leader && } - {loading && } -
    - - -
    -
    - -
    -
    -
    + + + {loading && } + + + + + ); }; return ( - - - {`${id} — ${i18n('tablet.header')} — ${ - tenantName || queryClusterName || CLUSTER_DEFAULT_TITLE - }`} - - {renderView()} - + + {renderHelmet()} + {renderPageMeta()} + + + {renderView()} + {renderTabs()} + {renderTabsContent()} + + + ); }; diff --git a/src/containers/Tablet/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/TabletInfo/TabletInfo.tsx deleted file mode 100644 index 9577cf95e..000000000 --- a/src/containers/Tablet/TabletInfo/TabletInfo.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import {Link as UIKitLink} from '@gravity-ui/uikit'; -import {Link} from 'react-router-dom'; - -import type {InfoViewerItem} from '../../../components/InfoViewer'; -import {InfoViewer} from '../../../components/InfoViewer'; -import routes, {createHref} from '../../../routes'; -import {ETabletState} from '../../../types/api/tablet'; -import type {TTabletStateInfo} from '../../../types/api/tablet'; -import {calcUptime} from '../../../utils/dataFormatters/dataFormatters'; -import {getDefaultNodePath} from '../../Node/NodePages'; -import {b} from '../Tablet'; - -interface TabletInfoProps { - tablet: TTabletStateInfo; - tenantPath?: string; -} - -export const TabletInfo = ({tablet, tenantPath}: TabletInfoProps) => { - const { - ChangeTime, - Generation, - FollowerId, - NodeId, - HiveId, - State, - Type, - TenantId: {SchemeShard} = {}, - } = tablet; - - const hasHiveId = HiveId && HiveId !== '0'; - const hasUptime = State === ETabletState.Active; - - const tabletInfo: InfoViewerItem[] = [{label: 'Database', value: tenantPath || '-'}]; - - if (hasHiveId) { - tabletInfo.push({ - label: 'HiveId', - value: ( - - {HiveId} - - ), - }); - } - - if (SchemeShard) { - tabletInfo.push({ - label: 'SchemeShard', - value: ( - - {SchemeShard} - - ), - }); - } - - tabletInfo.push({label: 'Type', value: Type}, {label: 'State', value: State}); - - if (hasUptime) { - tabletInfo.push({label: 'Uptime', value: calcUptime(ChangeTime)}); - } - - tabletInfo.push( - {label: 'Generation', value: Generation}, - { - label: 'Node', - value: ( - - {NodeId} - - ), - }, - ); - - if (FollowerId) { - tabletInfo.push({label: 'Follower', value: FollowerId}); - } - - return ; -}; diff --git a/src/containers/Tablet/TabletControls/TabletControls.tsx b/src/containers/Tablet/components/TabletControls/TabletControls.tsx similarity index 74% rename from src/containers/Tablet/TabletControls/TabletControls.tsx rename to src/containers/Tablet/components/TabletControls/TabletControls.tsx index 20ceb6ab9..d08ba5376 100644 --- a/src/containers/Tablet/TabletControls/TabletControls.tsx +++ b/src/containers/Tablet/components/TabletControls/TabletControls.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; -import {selectIsUserAllowedToMakeChanges} from '../../../store/reducers/authentication/authentication'; -import {ETabletState} from '../../../types/api/tablet'; -import type {TTabletStateInfo} from '../../../types/api/tablet'; -import {useTypedSelector} from '../../../utils/hooks'; -import {b} from '../Tablet'; -import i18n from '../i18n'; +import {ArrowRotateLeft, StopFill, TriangleRightFill} from '@gravity-ui/icons'; +import {Flex, Icon} from '@gravity-ui/uikit'; + +import {ButtonWithConfirmDialog} from '../../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; +import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication'; +import {ETabletState} from '../../../../types/api/tablet'; +import type {TTabletStateInfo} from '../../../../types/api/tablet'; +import {useTypedSelector} from '../../../../utils/hooks'; +import i18n from '../../i18n'; interface TabletControlsProps { tablet: TTabletStateInfo; @@ -28,9 +30,7 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { return window.api.resumeTablet(TabletId, HiveId); }; - const hasHiveId = () => { - return HiveId && HiveId !== '0'; - }; + const hasHiveId = HiveId && HiveId !== '0'; const isDisabledRestart = tablet.State === ETabletState.Stopped; @@ -41,50 +41,53 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { tablet.State === ETabletState.Stopped || tablet.State === ETabletState.Deleted; return ( -
    + + {i18n('controls.kill')} - {hasHiveId() ? ( + {hasHiveId && ( + {i18n('controls.stop')} + {i18n('controls.resume')} - ) : null} -
    + )} + ); }; diff --git a/src/containers/Tablet/TabletControls/index.ts b/src/containers/Tablet/components/TabletControls/index.ts similarity index 100% rename from src/containers/Tablet/TabletControls/index.ts rename to src/containers/Tablet/components/TabletControls/index.ts diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx new file mode 100644 index 000000000..7edbcf717 --- /dev/null +++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx @@ -0,0 +1,129 @@ +import {Flex, Link as UIKitLink} from '@gravity-ui/uikit'; +import {Link} from 'react-router-dom'; + +import type {InfoViewerItem} from '../../../../components/InfoViewer'; +import {InfoViewer} from '../../../../components/InfoViewer'; +import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; +import {useTypedSelector} from '../../../../lib'; +import {getTabletPagePath} from '../../../../routes'; +import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication'; +import {ETabletState} from '../../../../types/api/tablet'; +import type {TTabletStateInfo} from '../../../../types/api/tablet'; +import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; +import {createTabletDeveloperUIHref} from '../../../../utils/developerUI/developerUI'; +import {getDefaultNodePath} from '../../../Node/NodePages'; +import {b} from '../../shared'; +import {hasHive} from '../../utils'; + +import {tabletInfoKeyset} from './i18n'; + +interface TabletInfoProps { + tablet: TTabletStateInfo; +} + +export const TabletInfo = ({tablet}: TabletInfoProps) => { + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); + + const { + ChangeTime, + Generation, + FollowerId, + NodeId, + HiveId, + State, + TenantId: {SchemeShard} = {}, + TabletId, + } = tablet; + + const hasHiveId = hasHive(HiveId); + const hasUptime = State === ETabletState.Active; + + const tabletInfo: InfoViewerItem[] = []; + + if (hasHiveId) { + tabletInfo.push({ + label: tabletInfoKeyset('field_hive'), + value: ( + + {HiveId} + + ), + }); + } + + if (SchemeShard) { + tabletInfo.push({ + label: tabletInfoKeyset('field_scheme-shard'), + value: ( + + {SchemeShard} + + ), + }); + } + + tabletInfo.push({label: tabletInfoKeyset('field_state'), value: State}); + + if (hasUptime) { + tabletInfo.push({label: tabletInfoKeyset('field_uptime'), value: calcUptime(ChangeTime)}); + } + + tabletInfo.push( + {label: 'Generation', value: Generation}, + { + label: tabletInfoKeyset('field_node'), + value: ( + + {NodeId} + + ), + }, + ); + + if (FollowerId) { + tabletInfo.push({label: 'Follower', value: FollowerId}); + } + + const renderTabletInfo = () => { + return ; + }; + + const renderLinks = () => { + if (!isUserAllowedToMakeChanges || !TabletId) { + return null; + } + return ( +
    +
    Links
    + + + + + + +
    + ); + }; + + return ( + +
    +
    Info
    + {renderTabletInfo()} +
    + {renderLinks()} +
    + ); +}; diff --git a/src/containers/Tablet/components/TabletInfo/i18n/en.json b/src/containers/Tablet/components/TabletInfo/i18n/en.json new file mode 100644 index 000000000..25c27b5bf --- /dev/null +++ b/src/containers/Tablet/components/TabletInfo/i18n/en.json @@ -0,0 +1,12 @@ +{ + "field_scheme-shard": "SchemeShard", + "field_hive": "HiveId", + "field_state": "State", + "field_uptime": "Uptime", + "field_node": "Node", + "field_links": "Links", + "field_developer-ui-app": "App", + "field_developer-ui-counters": "Counters", + "field_developer-ui-executor": "Executor DB internals", + "field_developer-ui-state": "State Storage" +} diff --git a/src/containers/Tablet/components/TabletInfo/i18n/index.ts b/src/containers/Tablet/components/TabletInfo/i18n/index.ts new file mode 100644 index 000000000..a60df0b98 --- /dev/null +++ b/src/containers/Tablet/components/TabletInfo/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-tablet-info'; + +export const tabletInfoKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tablet/TabletInfo/index.ts b/src/containers/Tablet/components/TabletInfo/index.ts similarity index 100% rename from src/containers/Tablet/TabletInfo/index.ts rename to src/containers/Tablet/components/TabletInfo/index.ts diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss new file mode 100644 index 000000000..ba859a786 --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss @@ -0,0 +1,71 @@ +@import '../../../../styles/mixins.scss'; + +.ydb-tablet-storage-info { + $block: &; + &__table { + table-layout: fixed; + border-spacing: 0px; + border-collapse: collapse; + tr { + &:hover { + background-color: var(--g-color-base-generic-hover) !important; + } + } + tr:nth-of-type(2n + 1) { + background-color: var(--g-color-base-generic-ultralight); + } + } + + &__table-header-cell { + height: 40px; + padding: 0; + + text-align: left; + + background-color: var(--g-color-base-background); + @include text-subheader-2(); + &_align_right { + #{$block}__table-header-content { + justify-content: flex-end; + + text-align: right; + } + } + } + &__table-header-content { + display: flex; + align-items: center; + + height: 100%; + padding: var(--g-spacing-1) var(--g-spacing-2); + + vertical-align: middle; + + border-bottom: 1px solid var(--g-color-line-generic); + } + &__table-cell { + height: 30px; + padding: 0; + @include text-body-2(); + &_align_right { + text-align: right; + } + &_vertical-align_top { + vertical-align: top; + } + } + &__metrics-cell { + padding: var(--g-spacing-1) var(--g-spacing-2); + + white-space: nowrap; + } + &__name-wrapper { + padding: var(--g-spacing-1) var(--g-spacing-2); + } + &__with-padding { + padding-left: calc(var(--g-spacing-2) + var(--g-spacing-6)); + } + &__name-content_no-control { + padding-left: var(--g-spacing-6); + } +} diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx new file mode 100644 index 000000000..719face4a --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import {Table, useTable} from '@gravity-ui/table'; +import type {ExpandedState} from '@tanstack/react-table'; + +import type {TTabletHiveResponse} from '../../../../types/api/tablet'; + +import {getColumns} from './columns'; +import {b} from './shared'; +import {prepareData} from './utils'; + +import './TabletStorageInfo.scss'; + +interface TabletStorageInfoProps { + data?: TTabletHiveResponse | null; +} + +export function TabletStorageInfo({data}: TabletStorageInfoProps) { + const [expanded, setExpanded] = React.useState({}); + const tree = React.useMemo(() => prepareData(data), [data]); + const isExpandable = React.useMemo(() => tree.some((item) => item.children?.length), [tree]); + const columns = React.useMemo(() => getColumns(isExpandable), [isExpandable]); + const table = useTable({ + columns, + data: tree, + getSubRows: (item) => item.children, + enableExpanding: true, + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); + return ( + //block wrapper for table +
    + { + const align = column.columnDef.meta?.align; + return b('table-header-cell', {align}); + }} + cellClassName={(cell) => { + const align = cell?.column.columnDef.meta?.align; + const verticalAlign = cell?.column.columnDef.meta?.verticalAlign; + return b('table-cell', {align, 'vertical-align': verticalAlign}); + }} + className={b('table')} + /> + + ); +} diff --git a/src/containers/Tablet/components/TabletStorageInfo/columns.tsx b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx new file mode 100644 index 000000000..2ce5deddd --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx @@ -0,0 +1,105 @@ +import {ArrowToggle, Button, Flex} from '@gravity-ui/uikit'; +import type {CellContext, ColumnDef, Row} from '@tanstack/react-table'; + +import {formatTimestamp} from '../../../../utils/dataFormatters/dataFormatters'; + +import {tabletInfoKeyset} from './i18n'; +import {b} from './shared'; +import type {TabletStorageItem} from './types'; + +interface ColumnHeaderProps { + name: string; + className?: string; +} + +function ColumnHeader({name, className}: ColumnHeaderProps) { + return
    {name}
    ; +} + +function metricsCell( + info: CellContext, + formatter?: (value: string | number) => string | number, +) { + const value = info.getValue(); + const formattedValue = typeof formatter === 'function' ? formatter(value) : value; + + return
    {formattedValue}
    ; +} + +interface OperationCellProps { + row: Row; + name?: string; + hasExpand?: boolean; +} + +export function StoragePoolCell({row, name, hasExpand}: OperationCellProps) { + const isExpandable = row.getCanExpand(); + return ( + + {isExpandable && ( + + )} +
    + {name} +
    +
    + ); +} + +export function getColumns(hasExpand?: boolean) { + const columns: ColumnDef[] = [ + { + accessorKey: 'channelIndex', + header: () => , + size: 50, + cell: metricsCell, + meta: {align: 'right', verticalAlign: 'top'}, + }, + { + accessorKey: 'storagePoolName', + header: () => , + size: 200, + cell: metricsCell, + }, + { + accessorKey: 'GroupID', + header: () => ( + + ), + size: 100, + cell: (info) => ( + ()} + hasExpand={hasExpand} + /> + ), + meta: {verticalAlign: 'top'}, + }, + { + accessorKey: 'FromGeneration', + header: () => , + size: 100, + cell: metricsCell, + meta: {align: 'right', verticalAlign: 'top'}, + }, + { + accessorKey: 'Timestamp', + header: () => , + size: 200, + cell: (info) => metricsCell(info, formatTimestamp), + meta: {align: 'right', verticalAlign: 'top'}, + }, + ]; + return columns; +} diff --git a/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json b/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json new file mode 100644 index 000000000..12b9114bb --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/i18n/en.json @@ -0,0 +1,7 @@ +{ + "label_channel-index": "Channel", + "label_storage-pool": "Storage Pool Name", + "label_group-id": "Group ID", + "label_generation": "From generation", + "label_timestamp": "Timestamp" +} diff --git a/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts b/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts new file mode 100644 index 000000000..01ebbd1a5 --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-tablet-storage-info'; + +export const tabletInfoKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tablet/components/TabletStorageInfo/shared.ts b/src/containers/Tablet/components/TabletStorageInfo/shared.ts new file mode 100644 index 000000000..c219bf359 --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/shared.ts @@ -0,0 +1,3 @@ +import {cn} from '../../../../utils/cn'; + +export const b = cn('ydb-tablet-storage-info'); diff --git a/src/containers/Tablet/components/TabletStorageInfo/types.ts b/src/containers/Tablet/components/TabletStorageInfo/types.ts new file mode 100644 index 000000000..f2358378f --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/types.ts @@ -0,0 +1,6 @@ +import type {TTabletStorageInfoChannelHistory} from '../../../../types/api/tablet'; + +export interface TabletStorageItem extends TTabletStorageInfoChannelHistory { + storagePoolName?: string; + children?: TabletStorageItem[]; +} diff --git a/src/containers/Tablet/components/TabletStorageInfo/utils.ts b/src/containers/Tablet/components/TabletStorageInfo/utils.ts new file mode 100644 index 000000000..c7ed480b6 --- /dev/null +++ b/src/containers/Tablet/components/TabletStorageInfo/utils.ts @@ -0,0 +1,36 @@ +import type {TTabletHiveResponse} from '../../../../types/api/tablet'; + +import type {TabletStorageItem} from './types'; + +export function prepareData(data?: TTabletHiveResponse | null): TabletStorageItem[] { + if (!data) { + return []; + } + const { + BoundChannels, + TabletStorageInfo: {Channels}, + } = data; + + const result = []; + + for (const channel of Channels) { + const channelIndex = channel.Channel; + const history = [...channel.History]; + if (!history.length) { + continue; + } + + history.reverse(); + const [latest, ...rest] = history; + const storagePoolName = BoundChannels[channelIndex].StoragePoolName; + + const params = { + ...latest, + storagePoolName, + channelIndex, + children: rest, + }; + result.push(params); + } + return result; +} diff --git a/src/containers/Tablet/TabletTable/TabletTable.tsx b/src/containers/Tablet/components/TabletTable/TabletTable.tsx similarity index 79% rename from src/containers/Tablet/TabletTable/TabletTable.tsx rename to src/containers/Tablet/components/TabletTable/TabletTable.tsx index 5e096b1a7..67dae1ce5 100644 --- a/src/containers/Tablet/TabletTable/TabletTable.tsx +++ b/src/containers/Tablet/components/TabletTable/TabletTable.tsx @@ -1,12 +1,12 @@ import type {Column} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; -import {EntityStatus} from '../../../components/EntityStatus/EntityStatus'; -import {InternalLink} from '../../../components/InternalLink/InternalLink'; -import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable'; -import type {ITabletPreparedHistoryItem} from '../../../types/store/tablet'; -import {calcUptime} from '../../../utils/dataFormatters/dataFormatters'; -import {getDefaultNodePath} from '../../Node/NodePages'; +import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; +import {InternalLink} from '../../../../components/InternalLink/InternalLink'; +import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import type {ITabletPreparedHistoryItem} from '../../../../types/store/tablet'; +import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; +import {getDefaultNodePath} from '../../../Node/NodePages'; const TABLET_COLUMNS_WIDTH_LS_KEY = 'tabletTableColumnsWidth'; diff --git a/src/containers/Tablet/TabletTable/index.ts b/src/containers/Tablet/components/TabletTable/index.ts similarity index 100% rename from src/containers/Tablet/TabletTable/index.ts rename to src/containers/Tablet/components/TabletTable/index.ts diff --git a/src/containers/Tablet/i18n/en.json b/src/containers/Tablet/i18n/en.json index 2bea6d463..d24385939 100644 --- a/src/containers/Tablet/i18n/en.json +++ b/src/containers/Tablet/i18n/en.json @@ -1,6 +1,9 @@ { "tablet.header": "Tablet", + "tablet.meta-database": "Database", + "tablet.meta-follower": "Follower", + "controls.kill": "Restart", "controls.stop": "Stop", "controls.resume": "Resume", @@ -13,5 +16,8 @@ "dialog.stop": "The tablet will be stopped. Do you want to proceed?", "dialog.resume": "The tablet will be resumed. Do you want to proceed?", - "emptyState": "The tablet was not found" + "emptyState": "The tablet was not found", + + "label_tablet-history": "Tablets", + "label_tablet-channels": "Storage" } diff --git a/src/containers/Tablet/shared.ts b/src/containers/Tablet/shared.ts new file mode 100644 index 000000000..563d99454 --- /dev/null +++ b/src/containers/Tablet/shared.ts @@ -0,0 +1,3 @@ +import {cn} from '../../utils/cn'; + +export const b = cn('ydb-tablet-page'); diff --git a/src/containers/Tablet/utils.ts b/src/containers/Tablet/utils.ts new file mode 100644 index 000000000..d1c002eb6 --- /dev/null +++ b/src/containers/Tablet/utils.ts @@ -0,0 +1,3 @@ +export function hasHive(id?: string): id is string { + return Boolean(id && id !== '0'); +} diff --git a/src/services/api.ts b/src/services/api.ts index 1d3c5c1e0..5fd158287 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -38,6 +38,7 @@ import type {TEvSystemStateResponse} from '../types/api/systemState'; import type { TDomainKey, TEvTabletStateResponse, + TTabletHiveResponse, UnmergedTEvTabletStateResponse, } from '../types/api/tablet'; import type {TTenantInfo, TTenants} from '../types/api/tenant'; @@ -389,11 +390,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {concurrentId: concurrentId || 'getConsumer', requestConfig: {signal}}, ); } - getTablet({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) { + getTablet( + {id, database}: {id?: string; database?: string}, + {concurrentId, signal}: AxiosOptions = {}, + ) { return this.get( this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`), { enums: true, + database, }, { concurrentId, @@ -401,12 +406,16 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } - getTabletHistory({id}: {id?: string}, {concurrentId, signal}: AxiosOptions = {}) { + getTabletHistory( + {id, database}: {id?: string; database?: string}, + {concurrentId, signal}: AxiosOptions = {}, + ) { return this.get( this.getPath(`/viewer/json/tabletinfo?filter=(TabletId=${id})`), { enums: true, merge: false, + database, }, { concurrentId, @@ -414,11 +423,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } - getNodesList({concurrentId, signal}: AxiosOptions = {}) { + getNodesList({concurrentId, signal, database}: AxiosOptions & {database?: string} = {}) { return this.get( this.getPath('/viewer/json/nodelist'), { enums: true, + database, }, { concurrentId, @@ -586,6 +596,19 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {requestConfig: {'axios-retry': {retries: 0}}}, ); } + getTabletFromHive( + {id, hiveId}: {id?: string; hiveId?: string}, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.get>( + this.getPath(`/tablets/app?TabletID=${hiveId}&page=TabletInfo&tablet=${id}`), + {}, + { + concurrentId, + requestConfig: {signal}, + }, + ); + } getTabletDescribe(tenantId: TDomainKey, {concurrentId, signal}: AxiosOptions = {}) { return this.get>( this.getPath('/viewer/json/describe'), diff --git a/src/store/reducers/tablet.ts b/src/store/reducers/tablet.ts index 046768fc6..e7b5fe653 100644 --- a/src/store/reducers/tablet.ts +++ b/src/store/reducers/tablet.ts @@ -7,12 +7,12 @@ import {api} from './api'; export const tabletApi = api.injectEndpoints({ endpoints: (build) => ({ getTablet: build.query({ - queryFn: async ({id}: {id: string}, {signal}) => { + queryFn: async ({id, database}: {id: string; database?: string}, {signal}) => { try { const [tabletResponseData, historyResponseData, nodesList] = await Promise.all([ - window.api.getTablet({id}, {signal}), - window.api.getTabletHistory({id}, {signal}), - window.api.getNodesList({signal}), + window.api.getTablet({id, database}, {signal}), + window.api.getTabletHistory({id, database}, {signal}), + window.api.getNodesList({signal, database}), ]); const nodesMap = prepareNodesMap(nodesList); @@ -29,15 +29,17 @@ export const tabletApi = api.injectEndpoints({ const fqdn = nodesMap && nodeId ? nodesMap.get(Number(nodeId)) : undefined; - list.push({ - nodeId, - generation: Generation, - changeTime: ChangeTime, - state: State, - leader: Leader, - followerId: FollowerId, - fqdn, - }); + if (State !== 'Dead') { + list.push({ + nodeId, + generation: Generation, + changeTime: ChangeTime, + state: State, + leader: Leader, + followerId: FollowerId, + fqdn, + }); + } } return list; }, []); @@ -67,6 +69,21 @@ export const tabletApi = api.injectEndpoints({ }, providesTags: ['All'], }), + getAdvancedTableInfo: build.query({ + queryFn: async ({id, hiveId}: {id: string; hiveId?: string}, {signal}) => { + try { + const tabletResponseData = await window.api.getTabletFromHive( + {id, hiveId}, + {signal}, + ); + + return {data: tabletResponseData}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), }), overrideExisting: 'throw', }); diff --git a/src/types/api/tablet.ts b/src/types/api/tablet.ts index 76dcf5498..786b0ebf2 100644 --- a/src/types/api/tablet.ts +++ b/src/types/api/tablet.ts @@ -119,3 +119,26 @@ export enum ETabletState { 'Deleted' = 'Deleted', 'Stopped' = 'Stopped', } + +interface TBoundChannel { + IOPS: number; + StoragePoolName: string; + Throughput: number; + Size: number; +} + +export interface TTabletStorageInfoChannelHistory { + GroupID: number; + FromGeneration: number; + Timestamp: string; +} + +interface TTabletStorageInfoChannel { + Channel: number; + History: TTabletStorageInfoChannelHistory[]; +} + +export interface TTabletHiveResponse { + BoundChannels: TBoundChannel[]; + TabletStorageInfo: {Channels: TTabletStorageInfoChannel[]; Version: number}; +} diff --git a/src/types/store/tablets.ts b/src/types/store/tablets.ts index fd74ef908..befbff9bb 100644 --- a/src/types/store/tablets.ts +++ b/src/types/store/tablets.ts @@ -8,6 +8,7 @@ export interface TabletsState { export interface TabletsApiRequestParams { nodes?: string[]; path?: string; + database?: string; } export interface TabletsRootStateSlice { diff --git a/src/utils/dataFormatters/dataFormatters.ts b/src/utils/dataFormatters/dataFormatters.ts index 9a6b64cbd..cea9b2cc4 100644 --- a/src/utils/dataFormatters/dataFormatters.ts +++ b/src/utils/dataFormatters/dataFormatters.ts @@ -136,6 +136,11 @@ export const formatDateTime = (value?: number | string, defaultValue = '') => { return formattedData ?? defaultValue; }; +export const formatTimestamp = (value?: string | number, defaultValue = '') => { + const formattedData = dateTimeParse(value)?.format('YYYY-MM-DD HH:mm:ss.SSS'); + + return formattedData ?? defaultValue; +}; export const calcUptimeInSeconds = (milliseconds: number | string) => { const currentDate = new Date(); diff --git a/src/utils/developerUI/developerUI.ts b/src/utils/developerUI/developerUI.ts index 1a77e82ac..414fc4044 100644 --- a/src/utils/developerUI/developerUI.ts +++ b/src/utils/developerUI/developerUI.ts @@ -41,6 +41,12 @@ export const createVDiskDeveloperUILink = ({ return createDeveloperUILinkWithNodeId(nodeId, host) + vdiskPath; }; -export function createTabletDeveloperUIHref(tabletId: number | string, host = backend) { - return `${host}/tablets?TabletID=${tabletId}`; +export function createTabletDeveloperUIHref( + tabletId: number | string, + tabletPage?: string, + searchParam = 'TabletID', + host = backend, +) { + const subPage = tabletPage ? `/${tabletPage}` : ''; + return `${host}/tablets${subPage}?${searchParam}=${tabletId}`; } From fc3c2390f13752fbe7940384fbbc1ed5b349d1e2 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 19 Aug 2024 18:01:09 +0300 Subject: [PATCH 2/5] fix: review changes --- .../InfoViewerSkeleton/InfoViewerSkeleton.tsx | 13 +++++-- src/components/PDiskInfo/PDiskInfo.tsx | 2 +- src/components/TabletState/TabletState.tsx | 12 ++++++ src/containers/Tablet/Tablet.scss | 9 ----- src/containers/Tablet/Tablet.tsx | 13 ++++--- .../TabletControls/TabletControls.tsx | 3 +- .../components/TabletInfo/TabletInfo.scss | 12 ++++++ .../components/TabletInfo/TabletInfo.tsx | 38 +++++++++++-------- .../Tablet/components/TabletInfo/i18n/en.json | 6 ++- .../TabletStorageInfo/TabletStorageInfo.scss | 37 ++++++++++++------ .../components/TabletStorageInfo/columns.tsx | 17 +++------ .../components/TabletStorageInfo/utils.ts | 18 ++++----- .../components/TabletTable/TabletTable.tsx | 3 +- src/containers/Tablets/Tablets.tsx | 6 +-- src/services/api.ts | 11 ++++-- src/types/api/tablet.ts | 23 +++++------ 16 files changed, 136 insertions(+), 87 deletions(-) create mode 100644 src/components/TabletState/TabletState.tsx create mode 100644 src/containers/Tablet/components/TabletInfo/TabletInfo.scss diff --git a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx index f620eb4d5..cb10221f0 100644 --- a/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx +++ b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import {Skeleton} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; @@ -22,15 +24,20 @@ interface InfoViewerSkeletonProps { export const InfoViewerSkeleton = ({rows = 8, className, delay = 600}: InfoViewerSkeletonProps) => { const show = useDelayed(delay); + let skeletons: React.ReactNode = ( + + + + + ); if (!show) { - return null; + skeletons = null; } return (
    {[...new Array(rows)].map((_, index) => (
    - - + {skeletons}
    ))}
    diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx index 918988136..65b1ccc21 100644 --- a/src/components/PDiskInfo/PDiskInfo.tsx +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -164,7 +164,7 @@ function getPDiskInfo({ label: pDiskInfoKeyset('links'), value: ( - {isUserAllowedToMakeChanges && ( + {withPDiskPageLink && ( {state}; +} diff --git a/src/containers/Tablet/Tablet.scss b/src/containers/Tablet/Tablet.scss index 68dd4b0b4..6b13f19c1 100644 --- a/src/containers/Tablet/Tablet.scss +++ b/src/containers/Tablet/Tablet.scss @@ -16,13 +16,4 @@ &__loader { margin-left: var(--g-spacing-2); } - - &__link { - @extend .link; - } - - &__section-title { - margin: var(--g-spacing-1) 0 var(--g-spacing-3); - @include text-subheader-2(); - } } diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx index 5b837d716..5a6cbe8c9 100644 --- a/src/containers/Tablet/Tablet.tsx +++ b/src/containers/Tablet/Tablet.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Flex, Spin, Tabs} from '@gravity-ui/uikit'; +import {Flex, Tabs} from '@gravity-ui/uikit'; import {skipToken} from '@reduxjs/toolkit/query'; import {Helmet} from 'react-helmet-async'; import {useLocation, useParams} from 'react-router-dom'; @@ -93,10 +93,13 @@ export const Tablet = () => { const noAdvancedInfo = !isUserAllowedToMakeChanges || !hasHiveId; React.useEffect(() => { + if (loading) { + return; + } if (noAdvancedInfo && TABLET_PAGE_TABS.find(({id}) => id === tabletTab)?.isAdvanced) { setParams({activeTab: TABLET_TABS_IDS.history}); } - }, [noAdvancedInfo, tabletTab, setParams]); + }, [noAdvancedInfo, tabletTab, setParams, loading]); const { currentData: advancedData, @@ -208,9 +211,7 @@ export const Tablet = () => { entityName={i18n('tablet.header')} status={Overall ?? EFlag.Grey} id={TabletId} - > - {loading && } - + /> @@ -221,7 +222,7 @@ export const Tablet = () => { {renderHelmet()} {renderPageMeta()} - + { return window.api.resumeTablet(TabletId, HiveId); }; - const hasHiveId = HiveId && HiveId !== '0'; + const hasHiveId = hasHive(HiveId); const isDisabledRestart = tablet.State === ETabletState.Stopped; diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.scss b/src/containers/Tablet/components/TabletInfo/TabletInfo.scss new file mode 100644 index 000000000..b464aa735 --- /dev/null +++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.scss @@ -0,0 +1,12 @@ +@import '../../../../styles/mixins.scss'; + +.ydb-tablet-info { + &__link { + @extend .link; + } + + &__section-title { + margin: var(--g-spacing-1) 0 var(--g-spacing-3); + @include text-subheader-2(); + } +} diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx index 7edbcf717..dabdf617e 100644 --- a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx +++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx @@ -1,10 +1,11 @@ -import {Flex, Link as UIKitLink} from '@gravity-ui/uikit'; +import {Flex} from '@gravity-ui/uikit'; import {Link} from 'react-router-dom'; import type {InfoViewerItem} from '../../../../components/InfoViewer'; import {InfoViewer} from '../../../../components/InfoViewer'; import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; -import {useTypedSelector} from '../../../../lib'; +import {TabletState} from '../../../../components/TabletState/TabletState'; +import {cn, useTypedSelector} from '../../../../lib'; import {getTabletPagePath} from '../../../../routes'; import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication'; import {ETabletState} from '../../../../types/api/tablet'; @@ -12,11 +13,14 @@ import type {TTabletStateInfo} from '../../../../types/api/tablet'; import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; import {createTabletDeveloperUIHref} from '../../../../utils/developerUI/developerUI'; import {getDefaultNodePath} from '../../../Node/NodePages'; -import {b} from '../../shared'; import {hasHive} from '../../utils'; import {tabletInfoKeyset} from './i18n'; +const b = cn('ydb-tablet-info'); + +import './TabletInfo.scss'; + interface TabletInfoProps { tablet: TTabletStateInfo; } @@ -44,9 +48,9 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { tabletInfo.push({ label: tabletInfoKeyset('field_hive'), value: ( - + {HiveId} - + ), }); } @@ -55,21 +59,21 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { tabletInfo.push({ label: tabletInfoKeyset('field_scheme-shard'), value: ( - + {SchemeShard} - + ), }); } - tabletInfo.push({label: tabletInfoKeyset('field_state'), value: State}); + tabletInfo.push({label: tabletInfoKeyset('field_state'), value: }); if (hasUptime) { tabletInfo.push({label: tabletInfoKeyset('field_uptime'), value: calcUptime(ChangeTime)}); } tabletInfo.push( - {label: 'Generation', value: Generation}, + {label: tabletInfoKeyset('field_generation'), value: Generation}, { label: tabletInfoKeyset('field_node'), value: ( @@ -81,11 +85,16 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { ); if (FollowerId) { - tabletInfo.push({label: 'Follower', value: FollowerId}); + tabletInfo.push({label: tabletInfoKeyset('field_follower'), value: FollowerId}); } const renderTabletInfo = () => { - return ; + return ( +
    +
    {tabletInfoKeyset('title_info')}
    + +
    + ); }; const renderLinks = () => { @@ -94,7 +103,7 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { } return (
    -
    Links
    +
    {tabletInfoKeyset('title_links')}
    { return ( -
    -
    Info
    - {renderTabletInfo()} -
    + {renderTabletInfo()} {renderLinks()}
    ); diff --git a/src/containers/Tablet/components/TabletInfo/i18n/en.json b/src/containers/Tablet/components/TabletInfo/i18n/en.json index 25c27b5bf..2f4b5bb44 100644 --- a/src/containers/Tablet/components/TabletInfo/i18n/en.json +++ b/src/containers/Tablet/components/TabletInfo/i18n/en.json @@ -1,5 +1,7 @@ { "field_scheme-shard": "SchemeShard", + "field_follower": "Follower", + "field_generation": "Generation", "field_hive": "HiveId", "field_state": "State", "field_uptime": "Uptime", @@ -8,5 +10,7 @@ "field_developer-ui-app": "App", "field_developer-ui-counters": "Counters", "field_developer-ui-executor": "Executor DB internals", - "field_developer-ui-state": "State Storage" + "field_developer-ui-state": "State Storage", + "title_info": "Info", + "title_links": "Links" } diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss index ba859a786..dde7f36b6 100644 --- a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss +++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss @@ -14,16 +14,25 @@ tr:nth-of-type(2n + 1) { background-color: var(--g-color-base-generic-ultralight); } - } + :is(#{$block}__table-header-cell) { + height: 40px; + padding: 0; - &__table-header-cell { - height: 40px; - padding: 0; + text-align: left; - text-align: left; + background-color: var(--g-color-base-background); + @include text-subheader-2(); + // &_align_right { + // #{$block}__table-header-content { + // justify-content: flex-end; - background-color: var(--g-color-base-background); - @include text-subheader-2(); + // text-align: right; + // } + // } + } + } + + &__table-header-cell { &_align_right { #{$block}__table-header-content { justify-content: flex-end; @@ -32,6 +41,15 @@ } } } + :is(#{$block}__table-header-cell) { + height: 40px; + padding: 0; + + text-align: left; + + background-color: var(--g-color-base-background); + @include text-subheader-2(); + } &__table-header-content { display: flex; align-items: center; @@ -44,15 +62,12 @@ border-bottom: 1px solid var(--g-color-line-generic); } &__table-cell { - height: 30px; + height: 40px; padding: 0; @include text-body-2(); &_align_right { text-align: right; } - &_vertical-align_top { - vertical-align: top; - } } &__metrics-cell { padding: var(--g-spacing-1) var(--g-spacing-2); diff --git a/src/containers/Tablet/components/TabletStorageInfo/columns.tsx b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx index 2ce5deddd..54d9c9b0c 100644 --- a/src/containers/Tablet/components/TabletStorageInfo/columns.tsx +++ b/src/containers/Tablet/components/TabletStorageInfo/columns.tsx @@ -26,13 +26,13 @@ function metricsCell( return
    {formattedValue}
    ; } -interface OperationCellProps { +interface GroupIdCellProps { row: Row; name?: string; hasExpand?: boolean; } -export function StoragePoolCell({row, name, hasExpand}: OperationCellProps) { +function GroupIdCell({row, name, hasExpand}: GroupIdCellProps) { const isExpandable = row.getCanExpand(); return ( @@ -60,7 +60,7 @@ export function getColumns(hasExpand?: boolean) { header: () => , size: 50, cell: metricsCell, - meta: {align: 'right', verticalAlign: 'top'}, + meta: {align: 'right'}, }, { accessorKey: 'storagePoolName', @@ -78,27 +78,22 @@ export function getColumns(hasExpand?: boolean) { ), size: 100, cell: (info) => ( - ()} - hasExpand={hasExpand} - /> + ()} hasExpand={hasExpand} /> ), - meta: {verticalAlign: 'top'}, }, { accessorKey: 'FromGeneration', header: () => , size: 100, cell: metricsCell, - meta: {align: 'right', verticalAlign: 'top'}, + meta: {align: 'right'}, }, { accessorKey: 'Timestamp', header: () => , size: 200, cell: (info) => metricsCell(info, formatTimestamp), - meta: {align: 'right', verticalAlign: 'top'}, + meta: {align: 'right'}, }, ]; return columns; diff --git a/src/containers/Tablet/components/TabletStorageInfo/utils.ts b/src/containers/Tablet/components/TabletStorageInfo/utils.ts index c7ed480b6..272362e5f 100644 --- a/src/containers/Tablet/components/TabletStorageInfo/utils.ts +++ b/src/containers/Tablet/components/TabletStorageInfo/utils.ts @@ -6,23 +6,23 @@ export function prepareData(data?: TTabletHiveResponse | null): TabletStorageIte if (!data) { return []; } - const { - BoundChannels, - TabletStorageInfo: {Channels}, - } = data; + const {BoundChannels, TabletStorageInfo = {}} = data; + + const Channels = TabletStorageInfo.Channels ?? []; const result = []; for (const channel of Channels) { const channelIndex = channel.Channel; - const history = [...channel.History]; - if (!history.length) { + const channelHistory = channel.History; + if (!channelIndex || !channelHistory || !channelHistory.length) { continue; } + const copiedChannelHistory = [...channelHistory]; - history.reverse(); - const [latest, ...rest] = history; - const storagePoolName = BoundChannels[channelIndex].StoragePoolName; + copiedChannelHistory.reverse(); + const [latest, ...rest] = copiedChannelHistory; + const storagePoolName = BoundChannels?.[channelIndex]?.StoragePoolName; const params = { ...latest, diff --git a/src/containers/Tablet/components/TabletTable/TabletTable.tsx b/src/containers/Tablet/components/TabletTable/TabletTable.tsx index 67dae1ce5..588e472e9 100644 --- a/src/containers/Tablet/components/TabletTable/TabletTable.tsx +++ b/src/containers/Tablet/components/TabletTable/TabletTable.tsx @@ -4,6 +4,7 @@ import DataTable from '@gravity-ui/react-data-table'; import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; import {InternalLink} from '../../../../components/InternalLink/InternalLink'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {TabletState} from '../../../../components/TabletState/TabletState'; import type {ITabletPreparedHistoryItem} from '../../../../types/store/tablet'; import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; import {getDefaultNodePath} from '../../../Node/NodePages'; @@ -25,7 +26,7 @@ const columns: Column[] = [ { name: 'State', sortable: false, - render: ({row}) => row.state, + render: ({row}) => , }, { name: 'Follower ID', diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx index 7d6b2b194..270df62df 100644 --- a/src/containers/Tablets/Tablets.tsx +++ b/src/containers/Tablets/Tablets.tsx @@ -1,6 +1,6 @@ import {ArrowsRotateRight} from '@gravity-ui/icons'; import type {Column as DataTableColumn} from '@gravity-ui/react-data-table'; -import {Icon, Label, Text} from '@gravity-ui/uikit'; +import {Icon, Text} from '@gravity-ui/uikit'; import {skipToken} from '@reduxjs/toolkit/query'; import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; @@ -10,6 +10,7 @@ import {ResponseError} from '../../components/Errors/ResponseError'; import {InternalLink} from '../../components/InternalLink'; import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable'; import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton'; +import {TabletState} from '../../components/TabletState/TabletState'; import {getTabletPagePath} from '../../routes'; import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {selectTabletsWithFqdn, tabletsApi} from '../../store/reducers/tablets'; @@ -21,7 +22,6 @@ import {DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER} from '../../utils/consta import {calcUptime} from '../../utils/dataFormatters/dataFormatters'; import {createTabletDeveloperUIHref} from '../../utils/developerUI/developerUI'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; -import {mapTabletStateToLabelTheme} from '../../utils/tablet'; import {getDefaultNodePath} from '../Node/NodePages'; import i18n from './i18n'; @@ -78,7 +78,7 @@ const columns: DataTableColumn[] = [ return i18n('State'); }, render: ({row}) => { - return ; + return ; }, }, { diff --git a/src/services/api.ts b/src/services/api.ts index 5fd158287..d4a927fed 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -423,12 +423,11 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } - getNodesList({concurrentId, signal, database}: AxiosOptions & {database?: string} = {}) { + getNodesList({concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/nodelist'), { enums: true, - database, }, { concurrentId, @@ -601,8 +600,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {concurrentId, signal}: AxiosOptions = {}, ) { return this.get>( - this.getPath(`/tablets/app?TabletID=${hiveId}&page=TabletInfo&tablet=${id}`), - {}, + this.getPath('/tablets/app'), + { + TabletID: hiveId, + page: 'TabletInfo', + tablet: id, + }, { concurrentId, requestConfig: {signal}, diff --git a/src/types/api/tablet.ts b/src/types/api/tablet.ts index 786b0ebf2..7d5f49175 100644 --- a/src/types/api/tablet.ts +++ b/src/types/api/tablet.ts @@ -121,24 +121,25 @@ export enum ETabletState { } interface TBoundChannel { - IOPS: number; - StoragePoolName: string; - Throughput: number; - Size: number; + IOPS?: number; + StoragePoolName?: string; + Throughput?: number; + Size?: number; } export interface TTabletStorageInfoChannelHistory { - GroupID: number; - FromGeneration: number; - Timestamp: string; + GroupID?: number; + FromGeneration?: number; + Timestamp?: string; } interface TTabletStorageInfoChannel { - Channel: number; - History: TTabletStorageInfoChannelHistory[]; + Channel?: number; + History?: TTabletStorageInfoChannelHistory[]; } +//tablet data from hive export interface TTabletHiveResponse { - BoundChannels: TBoundChannel[]; - TabletStorageInfo: {Channels: TTabletStorageInfoChannel[]; Version: number}; + BoundChannels?: TBoundChannel[]; + TabletStorageInfo?: {Channels?: TTabletStorageInfoChannel[]; Version?: number}; } From 4cc96212f639f59955d9426235e42c2d04e6aae3 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 19 Aug 2024 18:04:16 +0300 Subject: [PATCH 3/5] fix: tablet reducer --- src/store/reducers/tablet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/reducers/tablet.ts b/src/store/reducers/tablet.ts index e7b5fe653..faa6f5d2b 100644 --- a/src/store/reducers/tablet.ts +++ b/src/store/reducers/tablet.ts @@ -12,7 +12,7 @@ export const tabletApi = api.injectEndpoints({ const [tabletResponseData, historyResponseData, nodesList] = await Promise.all([ window.api.getTablet({id, database}, {signal}), window.api.getTabletHistory({id, database}, {signal}), - window.api.getNodesList({signal, database}), + window.api.getNodesList({signal}), ]); const nodesMap = prepareNodesMap(nodesList); From 8070d3093d479ccf7a3a71fcb4a4de847652234e Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 19 Aug 2024 18:06:41 +0300 Subject: [PATCH 4/5] fix: styles --- .../components/TabletStorageInfo/TabletStorageInfo.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss index dde7f36b6..871c8331e 100644 --- a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss +++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss @@ -61,10 +61,12 @@ border-bottom: 1px solid var(--g-color-line-generic); } - &__table-cell { + :is(#{$block}__table-cell) { height: 40px; padding: 0; @include text-body-2(); + } + &__table-cell { &_align_right { text-align: right; } From 1395cd0c45c073491a2c96de2a01c9e5c1e3c861 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 19 Aug 2024 18:21:47 +0300 Subject: [PATCH 5/5] fix: fixes --- src/containers/Tablet/Tablet.tsx | 4 +++- .../components/TabletInfo/TabletInfo.tsx | 3 ++- .../TabletStorageInfo/TabletStorageInfo.scss | 20 +++++-------------- src/containers/Tablet/shared.ts | 3 --- 4 files changed, 10 insertions(+), 20 deletions(-) delete mode 100644 src/containers/Tablet/shared.ts diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx index 5a6cbe8c9..a8498c6e2 100644 --- a/src/containers/Tablet/Tablet.tsx +++ b/src/containers/Tablet/Tablet.tsx @@ -19,6 +19,7 @@ import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {tabletApi} from '../../store/reducers/tablet'; import {EFlag} from '../../types/api/enums'; import type {EType} from '../../types/api/tablet'; +import {cn} from '../../utils/cn'; import {CLUSTER_DEFAULT_TITLE} from '../../utils/constants'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; @@ -27,11 +28,12 @@ import {TabletInfo} from './components/TabletInfo'; import {TabletStorageInfo} from './components/TabletStorageInfo/TabletStorageInfo'; import {TabletTable} from './components/TabletTable'; import i18n from './i18n'; -import {b} from './shared'; import {hasHive} from './utils'; import './Tablet.scss'; +export const b = cn('ydb-tablet-page'); + const TABLET_TABS_IDS = { history: 'history', channels: 'channels', diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx index dabdf617e..1f1d491b9 100644 --- a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx +++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx @@ -5,13 +5,14 @@ import type {InfoViewerItem} from '../../../../components/InfoViewer'; import {InfoViewer} from '../../../../components/InfoViewer'; import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; import {TabletState} from '../../../../components/TabletState/TabletState'; -import {cn, useTypedSelector} from '../../../../lib'; import {getTabletPagePath} from '../../../../routes'; import {selectIsUserAllowedToMakeChanges} from '../../../../store/reducers/authentication/authentication'; import {ETabletState} from '../../../../types/api/tablet'; import type {TTabletStateInfo} from '../../../../types/api/tablet'; +import {cn} from '../../../../utils/cn'; import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; import {createTabletDeveloperUIHref} from '../../../../utils/developerUI/developerUI'; +import {useTypedSelector} from '../../../../utils/hooks'; import {getDefaultNodePath} from '../../../Node/NodePages'; import {hasHive} from '../../utils'; diff --git a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss index 871c8331e..48dd54865 100644 --- a/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss +++ b/src/containers/Tablet/components/TabletStorageInfo/TabletStorageInfo.scss @@ -22,13 +22,11 @@ background-color: var(--g-color-base-background); @include text-subheader-2(); - // &_align_right { - // #{$block}__table-header-content { - // justify-content: flex-end; - - // text-align: right; - // } - // } + } + :is(#{$block}__table-cell) { + height: 40px; + padding: 0; + @include text-body-2(); } } @@ -41,15 +39,7 @@ } } } - :is(#{$block}__table-header-cell) { - height: 40px; - padding: 0; - text-align: left; - - background-color: var(--g-color-base-background); - @include text-subheader-2(); - } &__table-header-content { display: flex; align-items: center; diff --git a/src/containers/Tablet/shared.ts b/src/containers/Tablet/shared.ts deleted file mode 100644 index 563d99454..000000000 --- a/src/containers/Tablet/shared.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {cn} from '../../utils/cn'; - -export const b = cn('ydb-tablet-page');