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-хука) без ручной сборки ServerTable props.

Когда не нужен:

EntitiesTable vs ServerAdminTable

EntitiesTableServerAdminTable
Вход колонокcolumnDefinitionscolumns + 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

Через 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

PropsEntitiesTableProps
PropTypeDefaultRequiredDescription
autoResetPageIndexbooleannoАвтоматический сброс пагинации к первой странице при изменении данных/фильтров/сортировки
bulkActionsBulkAction[]noСписок действий для массовых операций
cardColumnsnumbernoЖелаемое число колонок карточного вида (`view='cards'`). На широком контейнере рисуется ровно столько колонок; при сужении сетка схлопывается до меньшего числа (порог — `cardMinWidth`). Без пропа число колонок определяется только шириной контейнера и `cardMinWidth` (auto-fill).
cardMinWidthnumber320noМинимальная ширина карточки в `view='cards'`, px. Порог, ниже которого колонки схлопываются. Карточка ужимается до ширины контейнера, если он уже.
classNamestringnoCSS-класс
columnDefinitionsColumnDefinition<T>[]yesОпределение внешнего вида и функционала колонок
columnFilters(Omit<ChipChoiceRowProps<P>, "data-test-id" | "size"> & { open?: boolean; initialOpen?: boolean; onOpenChange?(isOpen: boolean): void; } & { ...; }) | undefinednoФильтры
columnVirtualizerInstanceRefMutableRefObject<ColumnVirtualizer>noRef на инстанс column-virtualizer'а для управления прокруткой снаружи
columnVirtualizerOptionsPartial<VirtualizerOptions<HTMLElement, Element>>noДополнительные параметры column-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=3).
copyPinnedRowsbooleanfalsenoПараметр отвечает за сохранение закрепленных строк в теле таблицы
data-test-idstringno
dataFilteredbooleannoФлаг, показывающий что данные были отфильтрованы при пустых данных
defaultLimitnumberno
defaultOffsetnumberno
defaultSearchstringno
defaultSortSortingStateno
defaultView"cards" | "table"'table' (на mobile — `cards`)noНачальный режим отображения (uncontrolled). Если не задан — дефолт по раскладке: `table` на desktop, `cards` на mobile (`TABLE_LAYOUT_PRESETS`).
enableColumnVirtualizationbooleanfalsenoВключает виртуализацию колонок (windowing по горизонтали). Рекомендуется при > 30 видимых колонок. Несовместимо с `view='cards'`. Pinned-колонки (left/right) всегда отрисовываются вне зависимости от настройки.
enableFuzzySearchbooleannoВключить нечеткий поиск
enableRowVirtualizationbooleanfalsenoВключает виртуализацию строк (windowing по вертикали). Рекомендуется при > 200 строк. Несовместимо с `view='cards'` — при картах игнорируется.
enableSelectPinnedbooleannoПараметр отвечает за чекбокс выбора закрепленных строк
errorDataStateEmptyStatePropsnoЭкран при ошибке запроса
expanding{ getSubRows: (element: T) => T[]; expandingColumnDefinition: TreeColumnDefinitionProps<T>; initialState?: ExpandedState; state?: ExpandedState | undefined; onChange?(state: ExpandedState): void; } | undefinednoОбщие настройки раскрывающихся (tree) строк: `getSubRows`, `expandingColumnDefinition`, `initialState`, `state`, `onChange`.
fullWidthbooleantruenoРастягивать таблицу на всю ширину контейнера. При `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Функция получения уникального идентификатора строки
hasMoreundefinedno
headerRowBackgroundColor"blue" | "green" | "neutral" | "orange" | "pink" | "red" | "violet" | "yellow"noAccent-тон фона строки заголовков колонок (`tableHeadLine`). Работает только в `view='table'`.
headlineIdstringnoId колонки, чей рендер используется как заголовок карточки в режиме `view='cards'`. Имеет смысл только при `view='cards'`.
idstringyes
infiniteLoadingundefinedno
keepPinnedRowsbooleanfalsenoПараметр отвечает за отображение закрепленных строк на всех страницах таблицы
layoutPresetsPartial<Record<LayoutType, Partial<TableLayoutDefaults>>>noOverride дефолтов адаптива для этого инстанса (`mergePresets` поверх `TABLE_LAYOUT_PRESETS`). `stickyControls` в пресете tier'а заменяет DS-объект целиком — указывайте все нужные поля. Escape-hatch: обычно не нужен — DS-пресет применяется автоматически по `AdaptiveProvider`.
loadMoreTriggerundefinedno
manualFilteringbooleanno
manualPaginationbooleanno
manualSortingbooleanno
moreActionsAction[]noЭлементы выпадающего списка кнопки с действиями
noDataStateEmptyStatePropsnoЭкран при отсутствии данных
noResultsStateEmptyStatePropsnoЭкран при отсутствии результатов поиска или фильтров
onExport(() => void)noКолбэк экспорта данных. Рендерит иконку в тулбаре перед настройками колонок.
onLoadMoreundefinedno
onPaginationOrDataChange((data: T[]) => void)no
onQuerySuccess(() => void)no
onRowClickRowClickHandler<T>noКолбэк клика по строке
onViewChange((view: View) => void)noКолбэк на смену режима отображения
queryFnEntityQueryFn<P, T>yes
queryPropsOmit<P, "params">no
refRef<EntitiesTableHandle<T>>no
renderCard((context: RenderCardContext<T>) => ReactNode)noКастомный рендер карточки в `view='cards'`. Получает контекст с tanstack `row` / `table` и `defaultRender` (готовый элемент дефолтной карточки — можно обернуть). Возврат заменяет дефолтную карточку.
rowAutoHeightbooleanno
rowPinningPick<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; } | undefinednoПараметры выбора строк: `initialState`, `state`, `enable`, `appearance`, `multiRow`, `onChange`.
rowVirtualizerInstanceRefMutableRefObject<RowVirtualizer>noRef на инстанс row-virtualizer'а для управления прокруткой снаружи
rowVirtualizerOptionsPartial<VirtualizerOptions<HTMLElement, Element>>noДополнительные параметры row-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=10, estimateSize=40).
scrollContainerRefRefObject<HTMLElement>noСсылка на контейнер, который скроллится
scrollRefRef<HTMLElement>noСсылка на элемент, обозначающий самый конец прокручиваемого списка
searchPlaceholderstringno
showDataViewbooleanfalsenoПоказывать переключатель вида (таблица/карточки) в тулбаре. Управляет только видимостью тоггла; сам вид задаётся `view` / `defaultView`. По умолчанию тоггла нет — таблица показывает один вид (`defaultView` либо адаптивный дефолт). Включите `showDataView`, чтобы дать пользователю переключать table/cards.
stickyControlsStickyControlsnoSticky-хром при скролле страницы: при `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.
suppressHeaderbooleannoОтключение хедера таблицы; в режиме `view='cards'` скрывает подписи-заголовки полей карточки
suppressPaginationbooleannoОтключение пагинации
suppressSearchbooleannoОтключение поиска
suppressToolbarbooleannoОтключение тулбара
toolbarAfterReactNodenoДополнительный слот в `Toolbar` после строки поиска
view"cards" | "table"'table' (на mobile — `cards`)noРежим отображения таблицы (controlled). `table` — классическая сетка; `cards` — карточки (заголовок берётся из колонки `headlineId`). Переключатель вида в тулбаре включается отдельным пропом `showDataView`.

Storybook

Смотри также

  • ServerAdminTable — ручной fetch и упрощённый API колонок.
  • ServerTable — escape hatch без product-дефолтов.