import React from 'react'; import {ArrowRotateLeft} from '@gravity-ui/icons'; import type {Column as DataTableColumn, SortOrder} from '@gravity-ui/react-data-table'; import {Icon, Select, Text} from '@gravity-ui/uikit'; import {isNil} from 'lodash'; import {useHistory} from 'react-router-dom'; import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; import {EntitiesCount} from '../../components/EntitiesCount'; import {EntityName} from '../../components/EntityName/EntityName'; import {ResponseError} from '../../components/Errors/ResponseError'; import {NodeId} from '../../components/NodeId/NodeId'; import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../components/Search/Search'; import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; import {TabletNameWrapper} from '../../components/TabletNameWrapper/TabletNameWrapper'; import {TabletState} from '../../components/TabletState/TabletState'; import {TabletUptime} from '../../components/UptimeViewer/UptimeViewer'; import {useTabletPagePath} from '../../routes'; import {tabletApi} from '../../store/reducers/tablet'; import {ETabletState} from '../../types/api/tablet'; import type {TTabletStateInfo} from '../../types/api/tablet'; import {DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import i18n from './i18n'; import {useTabletQueryParams} from './useTabletQueryParams'; import {filterTablets, getAvailableTabletTypes} from './utils'; function isFollowerTablet(state: TTabletStateInfo) { return state.Leader === false; } function getColumns({ nodeId, showEndOfRange, }: { nodeId?: string | number; showEndOfRange?: boolean; }) { const columns: DataTableColumn[] = [ { name: 'Type', width: 150, get header() { return i18n('Type'); }, render: ({row}) => { const isFollower = isFollowerTablet(row); return ( {row.Type} {isFollower ? follower : ''} ); }, }, { name: 'TabletId', width: 220, get header() { return i18n('Tablet'); }, render: ({row}) => { if (!row.TabletId) { return EMPTY_DATA_PLACEHOLDER; } return ( ); }, }, { name: 'State', get header() { return i18n('State'); }, render: ({row}) => { return ; }, }, ]; // For node page we don't need to show node columns if (nodeId === undefined) { columns.push( { name: 'NodeId', get header() { return i18n('Node ID'); }, render: ({row}) => { if (isNil(row.NodeId)) { return EMPTY_DATA_PLACEHOLDER; } return ; }, align: 'right', }, { name: 'fqdn', get header() { return i18n('Node FQDN'); }, render: ({row}) => { if (!row.fqdn) { return EMPTY_DATA_PLACEHOLDER; } return ; }, }, ); } columns.push( { name: 'Generation', get header() { return i18n('Generation'); }, align: 'right', }, { name: 'Uptime', get header() { return i18n('Uptime'); }, render: ({row}) => { return ; }, sortAccessor: (row) => -Number(row.ChangeTime), align: 'right', width: 120, }, ); if (nodeId === undefined && showEndOfRange) { columns.push({ name: 'EndOfRangeKeyPrefix', get header() { return i18n('End Range'); }, width: 350, render: ({row}) => { if (!row.EndOfRangeKeyPrefix) { return EMPTY_DATA_PLACEHOLDER; } return row.EndOfRangeKeyPrefix; }, }); } columns.push({ name: 'Actions', sortable: false, resizeable: false, header: '', render: ({row}) => { return ; }, }); return columns; } function TabletActions(tablet: TTabletStateInfo) { const isFollower = isFollowerTablet(tablet); const isDisabledRestart = tablet.State === ETabletState.Stopped || isFollower; const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); const [killTablet] = tabletApi.useKillTabletMutation(); const id = tablet.TabletId; if (!id) { return null; } let popoverContent: React.ReactNode; if (isFollower) { popoverContent = i18n('controls.kill-impossible-follower'); } else if (isUserAllowedToMakeChanges) { popoverContent = i18n('dialog.kill-header'); } else { popoverContent = i18n('controls.kill-not-allowed'); } return ( { return killTablet({id}).unwrap(); }} buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges} withPopover popoverContent={popoverContent} popoverPlacement={['right', 'bottom']} popoverDisabled={false} > ); } interface TabletsTableProps { tablets: (TTabletStateInfo & { fqdn?: string; })[]; className?: string; loading?: boolean; error?: unknown; scrollContainerRef: React.RefObject; nodeId?: string | number; /** * When true, the search value is applied on the backend and the table * should not filter the already-fetched tablets by `tabletsSearch`. */ backendSearchActive?: boolean; } export function TabletsTable({ tablets, loading, error, scrollContainerRef, nodeId, backendSearchActive, }: TabletsTableProps) { const {tabletsSearch, tabletTypes, handleTabletsSearchChange, handleTabletTypesChange} = useTabletQueryParams(); // Track sort state for scroll dependencies const [sortParams, setSortParams] = React.useState(); const {filteredTablets, showEndOfRange} = React.useMemo(() => { return { filteredTablets: filterTablets(tablets, { tabletIdSearch: backendSearchActive ? undefined : tabletsSearch, tabletTypes, }), showEndOfRange: tablets.some((tablet) => Boolean(tablet.EndOfRangeKeyPrefix)), }; }, [tablets, tabletsSearch, tabletTypes, backendSearchActive]); const tabletTypeOptions = React.useMemo(() => { return getAvailableTabletTypes(tablets, tabletTypes).map((tabletType) => ({ value: tabletType, content: tabletType, })); }, [tablets, tabletTypes]); const columns = React.useMemo( () => getColumns({nodeId, showEndOfRange}), [nodeId, showEndOfRange], ); const history = useHistory(); const getTabletPagePath = useTabletPagePath(); const openTablet = React.useCallback( (tablet: TTabletStateInfo) => { if (tablet.TabletId) { history.push( getTabletPagePath(tablet.TabletId, { followerId: tablet.FollowerId ? String(tablet.FollowerId) : undefined, }), ); } }, [getTabletPagePath, history], ); const filtersActive = Boolean(tabletsSearch.trim()) || tabletTypes.length > 0; return (