Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions src/containers/Tablets/Tablets.scss

This file was deleted.

248 changes: 144 additions & 104 deletions src/containers/Tablets/Tablets.tsx
Original file line number Diff line number Diff line change
@@ -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<TTabletStateInfo & {fqdn?: string}>[] = [
{
name: 'Type',
get header() {
return i18n('Type');
},
render: ({row}) => {
return (
<span>
{row.Type} {row.Leader ? <Text color="secondary">leader</Text> : ''}
</span>
);
},
},
{
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 <InternalLink to={tabletPath}>{row.TabletId}</InternalLink>;
},
},
{
name: 'State',
get header() {
return i18n('State');
},
render: ({row}) => {
return <Label theme={mapTabletStateToLabelTheme(row.State)}>{row.State}</Label>;
},
},
{
name: 'NodeId',
get header() {
return i18n('Node ID');
},
render: ({row}) => {
const nodePath = row.NodeId === undefined ? undefined : getDefaultNodePath(row.NodeId);
return <InternalLink to={nodePath}>{row.NodeId}</InternalLink>;
},
align: 'right',
},
{
name: 'FQDN',
get header() {
return i18n('Node FQDN');
},
render: ({row}) => {
if (!row.fqdn) {
return <span>—</span>;
}
return <EntityStatus name={row.fqdn} showStatus={false} hasClipboardButton />;
},
},
{
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 <TabletActions {...row} />;
},
},
];

function TabletActions(tablet: TTabletStateInfo) {
const isDisabledRestart = tablet.State === ETabletState.Stopped;
const dispatch = useTypedDispatch();
return (
<ButtonWithConfirmDialog
buttonView="outlined"
dialogContent={i18n('dialog.kill')}
onConfirmAction={() => {
return window.api.killTablet(tablet.TabletId);
}}
onConfirmActionSuccess={() => {
dispatch(tabletsApi.util.invalidateTags(['All']));
}}
buttonDisabled={isDisabledRestart}
>
<Icon data={ArrowsRotateRight} />
</ButtonWithConfirmDialog>
);
}

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};
}
Expand All @@ -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 <Tablet tablet={tabletsToRender[tabletIndex]} key={tabletIndex} />;
};

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 (
<div className={b(null, className)}>
<div className={b('header')}>
<Select
className={b('filter-control')}
multiple
placeholder={i18n('controls.allItems')}
label={`${i18n('controls.state')}:`}
options={states}
value={stateFilter}
onUpdate={handleStateFilterChange}
/>
<Select
className={b('filter-control')}
multiple
placeholder={i18n('controls.allItems')}
label={`${i18n('controls.type')}:`}
options={types}
value={typeFilter}
onUpdate={handleTypeFilterChange}
/>
<TabletsOverall tablets={tablets} />
</div>

<div className={b('items')}>
<ReactList
itemRenderer={renderTablet}
length={tabletsToRender.length}
type="uniform"
/>
</div>
</div>
);
};
const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, node, path));

if (loading) {
return <Loader />;
} else if (error) {
return <TableSkeleton />;
}
if (error) {
return <ResponseError error={error} />;
} else {
return tablets.length > 0 ? (
renderContent()
) : (
<div className="error">{i18n('noTabletsData')}</div>
);
}
};

return (
<div className={b(null, className)}>
<ResizeableDataTable
columns={columns}
data={tablets}
settings={DEFAULT_TABLE_SETTINGS}
emptyDataMessage={i18n('noTabletsData')}
/>
</div>
);
}
13 changes: 9 additions & 4 deletions src/containers/Tablets/i18n/en.json
Original file line number Diff line number Diff line change
@@ -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?"
}
3 changes: 1 addition & 2 deletions src/containers/Tablets/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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});
6 changes: 0 additions & 6 deletions src/containers/Tablets/i18n/ru.json

This file was deleted.

2 changes: 1 addition & 1 deletion src/containers/Tenant/Diagnostics/Diagnostics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down
Loading