From 75cd4fe25725371aadab0c6668c7fbf9ed990458 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Tue, 28 May 2024 12:43:15 +0000 Subject: [PATCH 1/2] feat(Diagnnostics): display tablets as table --- .../Tenant/Diagnostics/Diagnostics.tsx | 2 +- .../Tenant/Diagnostics/Tablets/Tablets.tsx | 171 ++++++++++++++++++ .../Tenant/Diagnostics/Tablets/i18n/en.json | 11 ++ .../Tenant/Diagnostics/Tablets/i18n/index.ts | 7 + src/store/reducers/tablets.ts | 34 +++- src/utils/tablet.ts | 17 ++ 6 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx create mode 100644 src/containers/Tenant/Diagnostics/Tablets/i18n/en.json create mode 100644 src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 1b56b61dc4..afd6aef76c 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -18,7 +18,6 @@ import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {Heatmap} from '../../Heatmap'; import {NodesWrapper} from '../../Nodes/NodesWrapper'; import {StorageWrapper} from '../../Storage/StorageWrapper'; -import {Tablets} from '../../Tablets'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {TenantTabsGroups} from '../TenantPages'; import {isDatabaseEntityType} from '../utils/schema'; @@ -31,6 +30,7 @@ import {DATABASE_PAGES, getPagesByType} from './DiagnosticsPages'; import {HotKeys} from './HotKeys/HotKeys'; import {Network} from './Network/Network'; import {Partitions} from './Partitions/Partitions'; +import {Tablets} from './Tablets/Tablets'; import {TopQueries} from './TopQueries'; import {TopShards} from './TopShards'; diff --git a/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx b/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx new file mode 100644 index 0000000000..b867003071 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx @@ -0,0 +1,171 @@ +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 {skipToken} from '@reduxjs/toolkit/query'; + +import {ButtonWithConfirmDialog} from '../../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; +import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {InternalLink} from '../../../../components/InternalLink'; +import {Loader} from '../../../../components/Loader'; +import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import routes, {createHref} from '../../../../routes'; +import {selectTabletsWithFqdn, tabletsApi} from '../../../../store/reducers/tablets'; +import {ETabletState} from '../../../../types/api/tablet'; +import type {TTabletStateInfo} from '../../../../types/api/tablet'; +import type {TabletsApiRequestParams} from '../../../../types/store/tablets'; +import {cn} from '../../../../utils/cn'; +import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; +import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {mapTabletStateToLabelTheme} from '../../../../utils/tablet'; +import {getDefaultNodePath} from '../../../Node/NodePages'; + +import i18n from './i18n'; + +const b = cn('tablets-table'); + +const columns: DataTableColumn[] = [ + { + name: 'Type', + get header() { + return i18n('Type'); + }, + render: ({row}) => { + return ( + + {row.Type} {row.Leader ? leader : ''} + + ); + }, + }, + { + name: 'TabletId', + get header() { + return i18n('Tablet'); + }, + render: ({row}) => { + const tabletPath = + row.TabletId && + createHref(routes.tablet, {id: row.TabletId}, {nodeId: row.NodeId, type: row.Type}); + + return {row.TabletId}; + }, + }, + { + name: 'State', + get header() { + return i18n('State'); + }, + render: ({row}) => { + return ; + }, + }, + { + name: 'NodeId', + get header() { + return i18n('Node ID'); + }, + render: ({row}) => { + const nodePath = row.NodeId === undefined ? undefined : getDefaultNodePath(row.NodeId); + return {row.NodeId}; + }, + align: 'right', + }, + { + name: 'FQDN', + get header() { + return i18n('Node FQDN'); + }, + render: ({row}) => { + if (!row.fqdn) { + return ; + } + return ; + }, + }, + { + name: 'Generation', + get header() { + return i18n('Generation'); + }, + align: 'right', + }, + { + name: 'Uptime', + get header() { + return i18n('Uptime'); + }, + render: ({row}) => { + return calcUptime(row.ChangeTime); + }, + sortAccessor: (row) => -Number(row.ChangeTime), + }, + { + name: 'Actions', + sortable: false, + resizeable: false, + header: '', + render: ({row}) => { + return ; + }, + }, +]; + +function TabletActions(tablet: TTabletStateInfo) { + const isDisabledRestart = tablet.State === ETabletState.Stopped; + const dispatch = useTypedDispatch(); + return ( + { + return window.api.killTablet(tablet.TabletId); + }} + onConfirmActionSuccess={() => { + dispatch(tabletsApi.util.invalidateTags(['All'])); + }} + buttonDisabled={isDisabledRestart} + > + + + ); +} + +interface TabletsProps { + path?: string; + className?: string; +} + +export function Tablets({path, className}: TabletsProps) { + const {autorefresh} = useTypedSelector((state) => state.schema); + + let params: TabletsApiRequestParams | typeof skipToken = skipToken; + if (path) { + params = {path}; + } + const {currentData, isFetching, error} = tabletsApi.useGetTabletsInfoQuery(params, { + pollingInterval: autorefresh, + }); + + const loading = isFetching && currentData === undefined; + const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, path || '')); + + if (loading) { + return ; + } + if (error) { + return ; + } + + return ( +
+ +
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/Tablets/i18n/en.json b/src/containers/Tenant/Diagnostics/Tablets/i18n/en.json new file mode 100644 index 0000000000..e0a6c9c404 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Tablets/i18n/en.json @@ -0,0 +1,11 @@ +{ + "noTabletsData": "No tablets data", + "Type": "Type", + "Tablet": "Tablet", + "State": "State", + "Node ID": "Node ID", + "Node FQDN": "Node FQDN", + "Generation": "Generation", + "Uptime": "Uptime", + "dialog.kill": "The tablet will be restarted. Do you want to proceed?" +} diff --git a/src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts b/src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts new file mode 100644 index 0000000000..79e3de6cd6 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-tablets-table'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/store/reducers/tablets.ts b/src/store/reducers/tablets.ts index f77e1e07f6..9992faea04 100644 --- a/src/store/reducers/tablets.ts +++ b/src/store/reducers/tablets.ts @@ -1,10 +1,12 @@ -import {createSlice} from '@reduxjs/toolkit'; +import {createSelector, createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; -import type {ETabletState, EType} from '../../types/api/tablet'; +import type {ETabletState, EType, TTabletStateInfo} from '../../types/api/tablet'; import type {TabletsApiRequestParams, TabletsState} from '../../types/store/tablets'; +import type {RootState} from '../defaultStore'; import {api} from './api'; +import {selectNodesMap} from './nodesList'; const initialState: TabletsState = { stateFilter: [], @@ -43,3 +45,31 @@ export const tabletsApi = api.injectEndpoints({ }), overrideExisting: 'throw', }); + +const getTabletsInfoSelector = createSelector( + (path: string) => path, + (path) => tabletsApi.endpoints.getTabletsInfo.select({path}), +); + +const selectGetTabletsInfo = createSelector( + (state: RootState) => state, + (_state: RootState, path: string) => getTabletsInfoSelector(path), + (state, selectGetNodeStructure) => selectGetNodeStructure(state).data, +); + +export const selectTabletsWithFqdn = createSelector( + (state: RootState, path: string) => selectGetTabletsInfo(state, path), + (state: RootState) => selectNodesMap(state), + (data, nodesMap): (TTabletStateInfo & {fqdn?: string})[] => { + if (!data?.TabletStateInfo) { + return []; + } + if (!nodesMap) { + return data as any; + } + return data.TabletStateInfo.map((tablet) => { + const fqdn = tablet.NodeId === undefined ? undefined : nodesMap.get(tablet.NodeId); + return {...tablet, fqdn}; + }); + }, +); diff --git a/src/utils/tablet.ts b/src/utils/tablet.ts index c1cf2ccd4f..d888af112b 100644 --- a/src/utils/tablet.ts +++ b/src/utils/tablet.ts @@ -1,3 +1,5 @@ +import type {LabelProps} from '@gravity-ui/uikit'; + import {EFlag} from '../types/api/enums'; import {ETabletState} from '../types/api/tablet'; @@ -52,3 +54,18 @@ export const mapTabletStateToColorState = (state?: ETabletState | EFlag): EFlag return tabletStateToColorState[state]; }; + +export function mapTabletStateToLabelTheme(state?: ETabletState): LabelProps['theme'] { + if (!state) { + return 'unknown'; + } + switch (state) { + case ETabletState.Dead: + return 'danger'; + case ETabletState.Active: + case ETabletState.Deleted: + return 'success'; + default: + return 'warning'; + } +} From 70d64c8f3af7b5d096ae654be7975a859fb5dea5 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Tue, 28 May 2024 17:11:26 +0000 Subject: [PATCH 2/2] fix: review --- src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx | 5 +++-- src/store/reducers/tablets.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx b/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx index b867003071..3dfc9d36f7 100644 --- a/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx +++ b/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx @@ -7,8 +7,8 @@ import {ButtonWithConfirmDialog} from '../../../../components/ButtonWithConfirmD import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {InternalLink} from '../../../../components/InternalLink'; -import {Loader} from '../../../../components/Loader'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton'; import routes, {createHref} from '../../../../routes'; import {selectTabletsWithFqdn, tabletsApi} from '../../../../store/reducers/tablets'; import {ETabletState} from '../../../../types/api/tablet'; @@ -100,6 +100,7 @@ const columns: DataTableColumn[] = [ return calcUptime(row.ChangeTime); }, sortAccessor: (row) => -Number(row.ChangeTime), + align: 'right', }, { name: 'Actions', @@ -152,7 +153,7 @@ export function Tablets({path, className}: TabletsProps) { const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, path || '')); if (loading) { - return ; + return ; } if (error) { return ; diff --git a/src/store/reducers/tablets.ts b/src/store/reducers/tablets.ts index 9992faea04..d27ecc403f 100644 --- a/src/store/reducers/tablets.ts +++ b/src/store/reducers/tablets.ts @@ -54,7 +54,7 @@ const getTabletsInfoSelector = createSelector( const selectGetTabletsInfo = createSelector( (state: RootState) => state, (_state: RootState, path: string) => getTabletsInfoSelector(path), - (state, selectGetNodeStructure) => selectGetNodeStructure(state).data, + (state, selectTabletsInfo) => selectTabletsInfo(state).data, ); export const selectTabletsWithFqdn = createSelector( @@ -65,7 +65,7 @@ export const selectTabletsWithFqdn = createSelector( return []; } if (!nodesMap) { - return data as any; + return data.TabletStateInfo; } return data.TabletStateInfo.map((tablet) => { const fqdn = tablet.NodeId === undefined ? undefined : nodesMap.get(tablet.NodeId);