From 7576d32fbe7ac19c1ba9438928e1405f63d3cbe0 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Wed, 29 May 2024 16:20:12 +0000 Subject: [PATCH] feat(Node): display tablets as a table --- src/containers/Tablets/Tablets.scss | 35 --- src/containers/Tablets/Tablets.tsx | 248 ++++++++++-------- src/containers/Tablets/i18n/en.json | 13 +- src/containers/Tablets/i18n/index.ts | 3 +- src/containers/Tablets/i18n/ru.json | 6 - .../Tenant/Diagnostics/Diagnostics.tsx | 2 +- .../Tenant/Diagnostics/Tablets/Tablets.tsx | 172 ------------ .../Tenant/Diagnostics/Tablets/i18n/en.json | 11 - .../Tenant/Diagnostics/Tablets/i18n/index.ts | 7 - src/store/reducers/tablets.ts | 13 +- 10 files changed, 164 insertions(+), 346 deletions(-) delete mode 100644 src/containers/Tablets/Tablets.scss delete mode 100644 src/containers/Tablets/i18n/ru.json delete mode 100644 src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx delete mode 100644 src/containers/Tenant/Diagnostics/Tablets/i18n/en.json delete mode 100644 src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts diff --git a/src/containers/Tablets/Tablets.scss b/src/containers/Tablets/Tablets.scss deleted file mode 100644 index 9ef4c0786d..0000000000 --- a/src/containers/Tablets/Tablets.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import '../../styles/mixins'; - -.tablets { - @include flex-container(); - - &__header { - display: flex; - align-items: center; - gap: 12px; - - margin-bottom: 16px; - } - - &__items { - flex: 1 1 auto; - } - - &__filters { - display: flex; - align-items: center; - } - - &__filter-control { - width: 180px; - min-width: 100px; - max-width: 180px; - } - - .tablet { - display: inline-block; - - line-height: 18px; - text-align: center; - } -} diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx index ef28c0dd56..89a616f4c9 100644 --- a/src/containers/Tablets/Tablets.tsx +++ b/src/containers/Tablets/Tablets.tsx @@ -1,40 +1,151 @@ -import React from 'react'; - -import {Select} from '@gravity-ui/uikit'; +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 ReactList from 'react-list'; +import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; +import {EntityStatus} from '../../components/EntityStatus/EntityStatus'; import {ResponseError} from '../../components/Errors/ResponseError'; -import {Loader} from '../../components/Loader'; -import {Tablet} from '../../components/Tablet'; -import TabletsOverall from '../../components/TabletsOverall/TabletsOverall'; -import {setStateFilter, setTypeFilter, tabletsApi} from '../../store/reducers/tablets'; -import type {ETabletState, EType} from '../../types/api/tablet'; +import {InternalLink} from '../../components/InternalLink'; +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'; +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'; -import './Tablets.scss'; - const b = cn('tablets'); +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), + align: 'right', + }, + { + 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; nodeId?: string | number; className?: string; } -export const Tablets = ({path, nodeId, className}: TabletsProps) => { - const dispatch = useTypedDispatch(); - - const {stateFilter, typeFilter} = useTypedSelector((state) => state.tablets); +export function Tablets({nodeId, path, className}: TabletsProps) { const {autorefresh} = useTypedSelector((state) => state.schema); let params: TabletsApiRequestParams | typeof skipToken = skipToken; - if (nodeId) { - params = {nodes: [String(nodeId)]}; + const node = nodeId === undefined ? undefined : String(nodeId); + if (node !== undefined) { + params = {nodes: [String(node)]}; } else if (path) { params = {path}; } @@ -43,94 +154,23 @@ export const Tablets = ({path, nodeId, className}: TabletsProps) => { }); const loading = isFetching && currentData === undefined; - const tablets = React.useMemo(() => currentData?.TabletStateInfo || [], [currentData]); - - const tabletsToRender = React.useMemo(() => { - let filteredTablets = tablets; - - if (typeFilter.length > 0) { - filteredTablets = filteredTablets.filter((tablet) => - typeFilter.some((filter) => tablet.Type === filter), - ); - } - if (stateFilter.length > 0) { - filteredTablets = filteredTablets.filter((tablet) => - stateFilter.some((filter) => tablet.State === filter), - ); - } - return filteredTablets; - }, [tablets, stateFilter, typeFilter]); - - const handleStateFilterChange = (value: string[]) => { - dispatch(setStateFilter(value as ETabletState[])); - }; - - const handleTypeFilterChange = (value: string[]) => { - dispatch(setTypeFilter(value as EType[])); - }; - - const renderTablet = (tabletIndex: number) => { - return ; - }; - - const renderContent = () => { - const states = Array.from(new Set(tablets.map((tablet) => tablet.State))) - .filter((state): state is ETabletState => state !== undefined) - .map((item) => ({ - value: item, - content: item, - })); - const types = Array.from(new Set(tablets.map((tablet) => tablet.Type))) - .filter((type): type is EType => type !== undefined) - .map((item) => ({ - value: item, - content: item, - })); - - return ( -
-
- - -
- -
- -
-
- ); - }; + const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, node, path)); if (loading) { - return ; - } else if (error) { + return ; + } + if (error) { return ; - } else { - return tablets.length > 0 ? ( - renderContent() - ) : ( -
{i18n('noTabletsData')}
- ); } -}; + + return ( +
+ +
+ ); +} diff --git a/src/containers/Tablets/i18n/en.json b/src/containers/Tablets/i18n/en.json index f3b130b68e..e0a6c9c404 100644 --- a/src/containers/Tablets/i18n/en.json +++ b/src/containers/Tablets/i18n/en.json @@ -1,6 +1,11 @@ { - "controls.type": "Type", - "controls.state": "State", - "controls.allItems": "All items", - "noTabletsData": "No tablets data" + "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/Tablets/i18n/index.ts b/src/containers/Tablets/i18n/index.ts index 4fbb01e210..900a82a4f7 100644 --- a/src/containers/Tablets/i18n/index.ts +++ b/src/containers/Tablets/i18n/index.ts @@ -1,8 +1,7 @@ import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; -import ru from './ru.json'; const COMPONENT = 'ydb-tablets'; -export default registerKeysets(COMPONENT, {ru, en}); +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tablets/i18n/ru.json b/src/containers/Tablets/i18n/ru.json deleted file mode 100644 index 2b182e9c38..0000000000 --- a/src/containers/Tablets/i18n/ru.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "controls.type": "Тип", - "controls.state": "Состояние", - "controls.allItems": "Все", - "noTabletsData": "Нет информации о таблетках" -} diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index e6cccf86ab..e3141e7c0a 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -17,6 +17,7 @@ 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'; @@ -29,7 +30,6 @@ 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 deleted file mode 100644 index 3dfc9d36f7..0000000000 --- a/src/containers/Tenant/Diagnostics/Tablets/Tablets.tsx +++ /dev/null @@ -1,172 +0,0 @@ -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 {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'; -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), - align: 'right', - }, - { - 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 deleted file mode 100644 index e0a6c9c404..0000000000 --- a/src/containers/Tenant/Diagnostics/Tablets/i18n/en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 79e3de6cd6..0000000000 --- a/src/containers/Tenant/Diagnostics/Tablets/i18n/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 d27ecc403f..de17e233e7 100644 --- a/src/store/reducers/tablets.ts +++ b/src/store/reducers/tablets.ts @@ -47,18 +47,23 @@ export const tabletsApi = api.injectEndpoints({ }); const getTabletsInfoSelector = createSelector( - (path: string) => path, - (path) => tabletsApi.endpoints.getTabletsInfo.select({path}), + (nodeId: string | undefined, path: string | undefined) => ({nodeId, path}), + ({nodeId, path}) => + tabletsApi.endpoints.getTabletsInfo.select( + nodeId === undefined ? {path} : {nodes: [nodeId]}, + ), ); const selectGetTabletsInfo = createSelector( (state: RootState) => state, - (_state: RootState, path: string) => getTabletsInfoSelector(path), + (_state: RootState, nodeId: string | undefined, path: string | undefined) => + getTabletsInfoSelector(nodeId, path), (state, selectTabletsInfo) => selectTabletsInfo(state).data, ); export const selectTabletsWithFqdn = createSelector( - (state: RootState, path: string) => selectGetTabletsInfo(state, path), + (state: RootState, nodeId: string | undefined, path: string | undefined) => + selectGetTabletsInfo(state, nodeId, path), (state: RootState) => selectNodesMap(state), (data, nodesMap): (TTabletStateInfo & {fqdn?: string})[] => { if (!data?.TabletStateInfo) {