EntitiesTable
EntitiesTable — product-preset над ServerTable для экранов «список сущностей с бэкенда»: локальный state (offset, limit, search, sorting), вызов queryFn и предустановленные дефолты (savedState, drag колонок, пагинация [10, 25, 50, 100]).
Когда использовать
- Миграция с
@cloud-ru/uikit-product-entities-table— тот же вход:columnDefinitions+queryFn. - Нужен единый glue для react-query (или совместимого query-хука) без ручной сборки
ServerTableprops.
Когда не нужен:
- Ручной fetch и упрощённый API (
columns,statusColumn) —ServerAdminTable. - Клиентские данные в памяти —
AdminTableилиSimpleTable.
EntitiesTable vs ServerAdminTable
EntitiesTable | ServerAdminTable | |
|---|---|---|
| Вход колонок | columnDefinitions | columns + statusColumn / rowActions |
| Данные | queryFn (custom hook на render) | items / total / loading вручную |
| State пагинации | внутри (useEntitiesTableState) | снаружи |
savedState | из обязательного id | не задаётся preset-ом |
| Drag колонок | включён по умолчанию | только меню настроек |
Оба preset-а валидны; выбор — по входному API и способу загрузки данных.
Переход на ServerAdminTable
Если экран укладывается в columns + statusColumn и вы сами управляете fetch — уберите queryFn, state страницы вынесите наружу, колонки упростите. savedState из id не переносится; drag — через columnsSettings.
// Было
<EntitiesTable id="entities" queryFn={useEntitiesQuery} columnDefinitions={columnDefinitions} getRowId={row => row.id} />
// Стало: items/total/loading + offset/limit/search снаружи
<ServerAdminTable
items={items}
total={total}
offset={offset}
limit={limit}
loading={loading}
columns={columns}
statusColumn={statusColumn}
getRowId={row => row.id}
onChangePage={(nextOffset, nextLimit) => { setOffset(nextOffset); setLimit(nextLimit); }}
search={{ state: search, onChange: value => { setOffset(0); setSearch(value); } }}
columnsSettings={{ enableDrag: true, enableSettingsMenu: true }}
/>
Примеры использования
Базовое использование
tsx
import {
ColumnDefinition,
EntitiesTable,
getRowActionsColumnDef,
getStatusColumnDef,
STATUS_APPEARANCE,
} from '@cloud-ru/ds-table';
type Entity = {
id: string;
name: string;
status: 'Active' | 'Paused';
owner: string;
};
const ENTITIES: Entity[] = [
{ id: 'e-1', name: 'Compute cluster', status: 'Active', owner: 'Anna' },
{ id: 'e-2', name: 'Object storage', status: 'Paused', owner: 'Boris' },
{ id: 'e-3', name: 'CDN edge', status: 'Active', owner: 'Vera' },
];
const columnDefinitions: ColumnDefinition<Entity>[] = [
getStatusColumnDef({
accessorKey: 'status',
header: 'Status',
size: 120,
mapStatusToAppearance: value => (value === 'Active' ? STATUS_APPEARANCE.Green : STATUS_APPEARANCE.Yellow),
renderDescription: value => String(value),
}),
{ id: 'name', accessorKey: 'name', header: 'Name', enableSorting: true },
{ id: 'owner', accessorKey: 'owner', header: 'Owner', enableSorting: true },
getRowActionsColumnDef({
actionsGenerator: cell => [{ content: { label: `Open ${cell.row.original.name}` }, onClick: () => {} }],
}),
];
function useEntitiesQuery({
params,
}: {
params: { offset: number; limit: number; search?: string; ordering?: string };
}) {
const normalizedSearch = params.search?.trim().toLowerCase() ?? '';
const filtered = normalizedSearch
? ENTITIES.filter(entity => entity.name.toLowerCase().includes(normalizedSearch))
: ENTITIES;
return {
data: {
total: filtered.length,
data: filtered.slice(params.offset, params.offset + params.limit),
},
isLoading: false,
isFetching: false,
isError: false,
isSuccess: true,
refetch: () => {},
};
}
export function EntitiesTableBasic() {
return (
<EntitiesTable<Entity, { params: { offset: number; limit: number; search?: string; ordering?: string } }>
id='entities-table-basic'
queryFn={useEntitiesQuery}
columnDefinitions={columnDefinitions}
defaultLimit={5}
searchPlaceholder='Search entities'
getRowId={entity => entity.id}
/>
);
}Через useEntitiesTableState + useEntitiesTableProps
tsx
import {
ColumnDefinition,
getRowActionsColumnDef,
getStatusColumnDef,
ServerTable,
STATUS_APPEARANCE,
useEntitiesTableProps,
useEntitiesTableState,
} from '@cloud-ru/ds-table';
type Entity = {
id: string;
name: string;
status: 'Active' | 'Paused';
owner: string;
};
const ENTITIES: Entity[] = [
{ id: 'e-1', name: 'Compute cluster', status: 'Active', owner: 'Anna' },
{ id: 'e-2', name: 'Object storage', status: 'Paused', owner: 'Boris' },
{ id: 'e-3', name: 'CDN edge', status: 'Active', owner: 'Vera' },
];
const columnDefinitions: ColumnDefinition<Entity>[] = [
getStatusColumnDef({
accessorKey: 'status',
header: 'Status',
size: 120,
mapStatusToAppearance: value => (value === 'Active' ? STATUS_APPEARANCE.Green : STATUS_APPEARANCE.Yellow),
renderDescription: value => String(value),
}),
{ id: 'name', accessorKey: 'name', header: 'Name', enableSorting: true },
{ id: 'owner', accessorKey: 'owner', header: 'Owner', enableSorting: true },
getRowActionsColumnDef({
actionsGenerator: cell => [{ content: { label: `Open ${cell.row.original.name}` }, onClick: () => {} }],
}),
];
function useEntitiesQuery({
params,
}: {
params: { offset: number; limit: number; search?: string; ordering?: string };
}) {
const normalizedSearch = params.search?.trim().toLowerCase() ?? '';
const filtered = normalizedSearch
? ENTITIES.filter(entity => entity.name.toLowerCase().includes(normalizedSearch))
: ENTITIES;
return {
data: {
total: filtered.length,
data: filtered.slice(params.offset, params.offset + params.limit),
},
isLoading: false,
isFetching: false,
isError: false,
isSuccess: true,
refetch: () => {},
};
}
export function EntitiesTableWithHook() {
const tableState = useEntitiesTableState({ defaultLimit: 5 });
const params = { params: tableState.paginationParams } as {
params: { offset: number; limit: number; search?: string; ordering?: string };
};
const query = useEntitiesQuery(params);
const tableProps = useEntitiesTableProps({
input: {
id: 'entities-table-with-hook',
columnDefinitions,
searchPlaceholder: 'Search entities',
getRowId: (entity: Entity) => entity.id,
},
tableState,
query,
});
return <ServerTable {...tableProps} />;
}С серверными фильтрами
tsx
import { FiltersState } from '@cloud-ru/ds-chips';
import {
ColumnDefinition,
EntitiesTable,
getRowActionsColumnDef,
getStatusColumnDef,
STATUS_APPEARANCE,
} from '@cloud-ru/ds-table';
type Entity = {
id: string;
name: string;
status: 'Active' | 'Paused' | 'Archived';
service: string;
};
type QueryParams = FiltersState & {
params: { offset: number; limit: number; search?: string; ordering?: string };
single?: string;
};
const ENTITIES: Entity[] = [
{ id: 'e-1', name: 'Compute cluster', status: 'Active', service: 'compute' },
{ id: 'e-2', name: 'Object storage', status: 'Paused', service: 'storage' },
{ id: 'e-3', name: 'CDN edge', status: 'Archived', service: 'cdn' },
];
const columnDefinitions: ColumnDefinition<Entity>[] = [
getStatusColumnDef({
accessorKey: 'status',
header: 'Status',
size: 120,
mapStatusToAppearance: value => {
if (value === 'Active') return STATUS_APPEARANCE.Green;
if (value === 'Paused') return STATUS_APPEARANCE.Yellow;
return STATUS_APPEARANCE.Neutral;
},
renderDescription: value => String(value),
}),
{ id: 'name', accessorKey: 'name', header: 'Name', enableSorting: true },
{ id: 'service', accessorKey: 'service', header: 'Service', enableSorting: true },
getRowActionsColumnDef({
actionsGenerator: cell => [{ content: { label: `Open ${cell.row.original.name}` }, onClick: () => {} }],
}),
];
function useEntitiesQuery(queryProps: QueryParams) {
const { params, single } = queryProps;
const filtered = ENTITIES.filter(entity => {
const matchesSearch = !params.search || entity.name.toLowerCase().includes(params.search.toLowerCase());
const matchesStatus = !single || entity.status.toLowerCase() === single.toLowerCase();
return matchesSearch && matchesStatus;
});
return {
data: {
total: filtered.length,
data: filtered.slice(params.offset, params.offset + params.limit),
},
isLoading: false,
isFetching: false,
isError: false,
isSuccess: true,
refetch: () => {},
};
}
export function EntitiesTableWithFilters() {
return (
<EntitiesTable<Entity, QueryParams>
id='entities-table-with-filters'
queryFn={useEntitiesQuery}
columnDefinitions={columnDefinitions}
defaultLimit={5}
searchPlaceholder='Search entities'
getRowId={entity => entity.id}
columnFilters={{
filters: [
{
id: 'single',
type: 'single',
label: 'Status',
pinned: true,
options: [
{ value: 'active', label: 'Active' },
{ value: 'paused', label: 'Paused' },
{ value: 'archived', label: 'Archived' },
],
},
],
}}
/>
);
}Props
Types
Props
EntitiesTableProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
autoResetPageIndex | boolean | — | no | Автоматический сброс пагинации к первой странице при изменении данных/фильтров/сортировки |
bulkActions | BulkAction[] | — | no | Список действий для массовых операций |
cardColumns | number | — | no | Желаемое число колонок карточного вида (`view='cards'`). На широком контейнере рисуется ровно столько колонок; при сужении сетка схлопывается до меньшего числа (порог — `cardMinWidth`). Без пропа число колонок определяется только шириной контейнера и `cardMinWidth` (auto-fill). |
cardMinWidth | number | 320 | no | Минимальная ширина карточки в `view='cards'`, px. Порог, ниже которого колонки схлопываются. Карточка ужимается до ширины контейнера, если он уже. |
className | string | — | no | CSS-класс |
columnDefinitions | ColumnDefinition<T>[] | — | yes | Определение внешнего вида и функционала колонок |
columnFilters | (Omit<ChipChoiceRowProps<P>, "data-test-id" | "size"> & { open?: boolean; initialOpen?: boolean; onOpenChange?(isOpen: boolean): void; } & { ...; }) | undefined | — | no | Фильтры |
columnVirtualizerInstanceRef | MutableRefObject<ColumnVirtualizer> | — | no | Ref на инстанс column-virtualizer'а для управления прокруткой снаружи |
columnVirtualizerOptions | Partial<VirtualizerOptions<HTMLElement, Element>> | — | no | Дополнительные параметры column-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=3). |
copyPinnedRows | boolean | false | no | Параметр отвечает за сохранение закрепленных строк в теле таблицы |
data-test-id | string | — | no | |
dataFiltered | boolean | — | no | Флаг, показывающий что данные были отфильтрованы при пустых данных |
defaultLimit | number | — | no | |
defaultOffset | number | — | no | |
defaultSearch | string | — | no | |
defaultSort | SortingState | — | no | |
defaultView | "cards" | "table" | 'table' (на mobile — `cards`) | no | Начальный режим отображения (uncontrolled). Если не задан — дефолт по раскладке: `table` на desktop, `cards` на mobile (`TABLE_LAYOUT_PRESETS`). |
enableColumnVirtualization | boolean | false | no | Включает виртуализацию колонок (windowing по горизонтали). Рекомендуется при > 30 видимых колонок. Несовместимо с `view='cards'`. Pinned-колонки (left/right) всегда отрисовываются вне зависимости от настройки. |
enableFuzzySearch | boolean | — | no | Включить нечеткий поиск |
enableRowVirtualization | boolean | false | no | Включает виртуализацию строк (windowing по вертикали). Рекомендуется при > 200 строк. Несовместимо с `view='cards'` — при картах игнорируется. |
enableSelectPinned | boolean | — | no | Параметр отвечает за чекбокс выбора закрепленных строк |
errorDataState | EmptyStateProps | — | no | Экран при ошибке запроса |
expanding | { getSubRows: (element: T) => T[]; expandingColumnDefinition: TreeColumnDefinitionProps<T>; initialState?: ExpandedState; state?: ExpandedState | undefined; onChange?(state: ExpandedState): void; } | undefined | — | no | Общие настройки раскрывающихся (tree) строк: `getSubRows`, `expandingColumnDefinition`, `initialState`, `state`, `onChange`. |
fullWidth | boolean | true | no | Растягивать таблицу на всю ширину контейнера. При `false` ширина определяется суммой колонок (лучше всего, когда у всех колонок задан `size` / `width`). Явный проп = desktop-значение; на mobile всегда `true` (`TABLE_LAYOUT_PRESETS`). |
getRowBackgroundColor | ((data: T) => TableRowColor) | — | no | Функция определения цвета фона строки по её данным. Работает только в `view='table'` — карточки (`view='cards'`) не тонируются. @param data данные строки @returns цвет фона строки или `undefined` |
getRowId | ((originalRow: T, index: number, parent?: Row<T>) => string) | — | no | Функция получения уникального идентификатора строки |
hasMore | undefined | — | no | |
headerRowBackgroundColor | "blue" | "green" | "neutral" | "orange" | "pink" | "red" | "violet" | "yellow" | — | no | Accent-тон фона строки заголовков колонок (`tableHeadLine`). Работает только в `view='table'`. |
headlineId | string | — | no | Id колонки, чей рендер используется как заголовок карточки в режиме `view='cards'`. Имеет смысл только при `view='cards'`. |
id | string | — | yes | |
infiniteLoading | undefined | — | no | |
keepPinnedRows | boolean | false | no | Параметр отвечает за отображение закрепленных строк на всех страницах таблицы |
layoutPresets | Partial<Record<LayoutType, Partial<TableLayoutDefaults>>> | — | no | Override дефолтов адаптива для этого инстанса (`mergePresets` поверх `TABLE_LAYOUT_PRESETS`). `stickyControls` в пресете tier'а заменяет DS-объект целиком — указывайте все нужные поля. Escape-hatch: обычно не нужен — DS-пресет применяется автоматически по `AdaptiveProvider`. |
loadMoreTrigger | undefined | — | no | |
manualFiltering | boolean | — | no | |
manualPagination | boolean | — | no | |
manualSorting | boolean | — | no | |
moreActions | Action[] | — | no | Элементы выпадающего списка кнопки с действиями |
noDataState | EmptyStateProps | — | no | Экран при отсутствии данных |
noResultsState | EmptyStateProps | — | no | Экран при отсутствии результатов поиска или фильтров |
onExport | (() => void) | — | no | Колбэк экспорта данных. Рендерит иконку в тулбаре перед настройками колонок. |
onLoadMore | undefined | — | no | |
onPaginationOrDataChange | ((data: T[]) => void) | — | no | |
onQuerySuccess | (() => void) | — | no | |
onRowClick | RowClickHandler<T> | — | no | Колбэк клика по строке |
onViewChange | ((view: View) => void) | — | no | Колбэк на смену режима отображения |
queryFn | EntityQueryFn<P, T> | — | yes | |
queryProps | Omit<P, "params"> | — | no | |
ref | Ref<EntitiesTableHandle<T>> | — | no | |
renderCard | ((context: RenderCardContext<T>) => ReactNode) | — | no | Кастомный рендер карточки в `view='cards'`. Получает контекст с tanstack `row` / `table` и `defaultRender` (готовый элемент дефолтной карточки — можно обернуть). Возврат заменяет дефолтную карточку. |
rowAutoHeight | boolean | — | no | |
rowPinning | Pick<RowPinningState, "top"> | — | no | Определение, какие строки должны быть закреплены в таблице |
rowSelection | { initialState?: RowSelectionState; state?: RowSelectionState; enable?: boolean | ((row: Row<T>) => boolean) | undefined; multiRow?: boolean | undefined; onChange?(state: RowSelectionState): void; appearance?: RowAppearance | undefined; } | undefined | — | no | Параметры выбора строк: `initialState`, `state`, `enable`, `appearance`, `multiRow`, `onChange`. |
rowVirtualizerInstanceRef | MutableRefObject<RowVirtualizer> | — | no | Ref на инстанс row-virtualizer'а для управления прокруткой снаружи |
rowVirtualizerOptions | Partial<VirtualizerOptions<HTMLElement, Element>> | — | no | Дополнительные параметры row-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=10, estimateSize=40). |
scrollContainerRef | RefObject<HTMLElement> | — | no | Ссылка на контейнер, который скроллится |
scrollRef | Ref<HTMLElement> | — | no | Ссылка на элемент, обозначающий самый конец прокручиваемого с писка |
searchPlaceholder | string | — | no | |
showDataView | boolean | false | no | Показывать переключатель вида (таблица/карточки) в тулбаре. Управляет только видимостью тоггла; сам вид задаётся `view` / `defaultView`. По умолчанию тоггла нет — таблица показывает один вид (`defaultView` либо адаптивный дефолт). Включите `showDataView`, чтобы дать пользователю переключать table/cards. |
stickyControls | StickyControls | — | no | Sticky-хром при скролле страницы: при `enabled: true` тулбар и пагинация липнут к верху/низу viewport, в table-view заголовок колонок — под тулбаром; тело растёт по контенту. При `enabled: false` все блоки идут сплошным потоком без sticky. Дефолты: desktop — `enabled: false` (offsets не применяются); mobile — `{ enabled: true, offsetTop: 0, offsetBottom: 0 }` (`TABLE_LAYOUT_PRESETS`); `backgroundPredefined` — `neutralBackground1Level` на всех раскладках. Явный проп = desktop-значение; mobile-override — `layoutPresets.mobile`. @example `stickyControls={{ enabled: true, offsetTop: 64 }}` — sticky на desktop, app header 64px. |
suppressHeader | boolean | — | no | Отключение хедера таблицы; в режиме `view='cards'` скрывает подписи-заголовки полей карточки |
suppressPagination | boolean | — | no | Отключение пагинации |
suppressSearch | boolean | — | no | Отключение поиска |
suppressToolbar | boolean | — | no | Отключение тулбара |
toolbarAfter | ReactNode | — | no | Дополнительный слот в `Toolbar` после строки поиска |
view | "cards" | "table" | 'table' (на mobile — `cards`) | no | Режим отображения таблицы (controlled). `table` — классическая сетка; `cards` — карточки (заголовок берётся из колонки `headlineId`). Переключатель вида в тулбаре включается отдельным пропом `showDataView`. |
Storybook
Смотри также
ServerAdminTable— ручной fetch и упрощённый API колонок.ServerTable— escape hatch без product-дефолтов.