diff --git a/.changeset/every-lions-pay.md b/.changeset/every-lions-pay.md
new file mode 100644
index 0000000000..9c5a32f4e4
--- /dev/null
+++ b/.changeset/every-lions-pay.md
@@ -0,0 +1,44 @@
+---
+'@backstage/ui': minor
+---
+
+**BREAKING**: Redesigned Table component with new `useTable` hook API.
+
+- The `Table` component (React Aria wrapper) is renamed to `TableRoot`
+- New high-level `Table` component that handles data display, pagination, sorting, and selection
+- The `useTable` hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor)
+- New types: `ColumnConfig`, `TableProps`, `TableItem`, `UseTableOptions`, `UseTableResult`
+
+New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors.
+
+**Migration guide:**
+
+1. Update imports and use the new `useTable` hook:
+
+```diff
+-import { Table, useTable } from '@backstage/ui';
+-const { data, paginationProps } = useTable({ data: items, pagination: {...} });
++import { Table, useTable, type ColumnConfig } from '@backstage/ui';
++const { tableProps } = useTable({
++ mode: 'complete',
++ getData: () => items,
++});
+```
+
+2. Define columns and render with the new Table API:
+
+```diff
+-
+-
++const columns: ColumnConfig- [] = [
++ { id: 'name', label: 'Name', isRowHeader: true, cell: item =>
},
++ { id: 'type', label: 'Type', cell: item => },
++];
++
++
+```
+
+Affected components: Table, TableRoot, TablePagination
diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt
index 44ee42a25a..d8c25565ee 100644
--- a/.github/vale/config/vocabularies/Backstage/accept.txt
+++ b/.github/vale/config/vocabularies/Backstage/accept.txt
@@ -99,6 +99,7 @@ dataflow
dataloader
dayjs
debounce
+debounced
debounces
debuggability
declaratively
diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx
index 0cb6119ce6..0507c85c9f 100644
--- a/docs-ui/src/snippets/stories-snippets.tsx
+++ b/docs-ui/src/snippets/stories-snippets.tsx
@@ -26,7 +26,7 @@ import * as SkeletonStories from '../../../packages/ui/src/components/Skeleton/S
import * as CardStories from '../../../packages/ui/src/components/Card/Card.stories';
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
-import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
+import * as TableStories from '../../../packages/ui/src/components/Table/stories/Table.docs.stories';
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
import * as VisuallyHiddenStories from '../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories';
diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md
index 0a7e289d0a..b7b4d4dc05 100644
--- a/packages/ui/report.api.md
+++ b/packages/ui/report.api.md
@@ -37,11 +37,12 @@ import { RowProps } from 'react-aria-components';
import type { SearchFieldProps as SearchFieldProps_2 } from 'react-aria-components';
import type { SelectProps as SelectProps_2 } from 'react-aria-components';
import type { SeparatorProps } from 'react-aria-components';
+import type { SortDescriptor as SortDescriptor_2 } from 'react-stately';
import type { SubmenuTriggerProps as SubmenuTriggerProps_2 } from 'react-aria-components';
import type { SwitchProps as SwitchProps_2 } from 'react-aria-components';
import { TableBodyProps } from 'react-aria-components';
import { TableHeaderProps } from 'react-aria-components';
-import { TableProps } from 'react-aria-components';
+import { TableProps as TableProps_2 } from 'react-aria-components';
import type { TabListProps as TabListProps_2 } from 'react-aria-components';
import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components';
import type { TabProps as TabProps_2 } from 'react-aria-components';
@@ -456,6 +457,26 @@ export type ClassNamesMap = Record;
// @public (undocumented)
export const Column: (props: ColumnProps) => JSX_2.Element;
+// @public (undocumented)
+export interface ColumnConfig {
+ // (undocumented)
+ cell: (item: T) => ReactNode;
+ // (undocumented)
+ header?: () => ReactNode;
+ // (undocumented)
+ id: string;
+ // (undocumented)
+ isHidden?: boolean;
+ // (undocumented)
+ isRowHeader?: boolean;
+ // (undocumented)
+ isSortable?: boolean;
+ // (undocumented)
+ label: string;
+ // (undocumented)
+ width?: number | string;
+}
+
// @public (undocumented)
export interface ColumnProps extends Omit {
// (undocumented)
@@ -523,6 +544,34 @@ export interface ContainerProps {
style?: React.CSSProperties;
}
+// @public (undocumented)
+export interface CursorParams {
+ // (undocumented)
+ cursor: string | undefined;
+ // (undocumented)
+ filter: TFilter | undefined;
+ // (undocumented)
+ pageSize: number;
+ // (undocumented)
+ search: string;
+ // (undocumented)
+ signal: AbortSignal;
+ // (undocumented)
+ sort: SortDescriptor | null;
+}
+
+// @public (undocumented)
+export interface CursorResponse {
+ // (undocumented)
+ data: T[];
+ // (undocumented)
+ nextCursor?: string;
+ // (undocumented)
+ prevCursor?: string;
+ // (undocumented)
+ totalCount?: number;
+}
+
// @public
export type DataAttributesMap = Record;
@@ -627,6 +676,14 @@ export interface FieldLabelProps
secondaryLabel?: string | null;
}
+// @public (undocumented)
+export interface FilterState {
+ // (undocumented)
+ onFilterChange: (filter: TFilter) => void;
+ // (undocumented)
+ value: TFilter | undefined;
+}
+
// @public (undocumented)
export const Flex: ForwardRefExoticComponent<
FlexProps & RefAttributes
@@ -1038,6 +1095,36 @@ export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element;
// @public (undocumented)
export interface MenuTriggerProps extends MenuTriggerProps_2 {}
+// @public (undocumented)
+export interface NoPagination {
+ // (undocumented)
+ type: 'none';
+}
+
+// @public (undocumented)
+export interface OffsetParams {
+ // (undocumented)
+ filter: TFilter | undefined;
+ // (undocumented)
+ offset: number;
+ // (undocumented)
+ pageSize: number;
+ // (undocumented)
+ search: string;
+ // (undocumented)
+ signal: AbortSignal;
+ // (undocumented)
+ sort: SortDescriptor | null;
+}
+
+// @public (undocumented)
+export interface OffsetResponse {
+ // (undocumented)
+ data: T[];
+ // (undocumented)
+ totalCount: number;
+}
+
// @public (undocumented)
type Option_2 = {
value: string;
@@ -1046,6 +1133,46 @@ type Option_2 = {
};
export { Option_2 as Option };
+// @public (undocumented)
+export interface PagePagination extends TablePaginationProps {
+ // (undocumented)
+ type: 'page';
+}
+
+// @public (undocumented)
+export interface PaginationOptions {
+ // (undocumented)
+ getLabel?: TablePaginationProps['getLabel'];
+ // (undocumented)
+ initialOffset?: number;
+ // (undocumented)
+ pageSize?: number;
+ // (undocumented)
+ showPageSizeOptions?: boolean;
+}
+
+// @public (undocumented)
+export interface QueryOptions {
+ // (undocumented)
+ filter?: TFilter;
+ // (undocumented)
+ initialFilter?: TFilter;
+ // (undocumented)
+ initialSearch?: string;
+ // (undocumented)
+ initialSort?: SortDescriptor;
+ // (undocumented)
+ onFilterChange?: (filter: TFilter) => void;
+ // (undocumented)
+ onSearchChange?: (search: string) => void;
+ // (undocumented)
+ onSortChange?: (sort: SortDescriptor) => void;
+ // (undocumented)
+ search?: string;
+ // (undocumented)
+ sort?: SortDescriptor | null;
+}
+
// @public (undocumented)
export const Radio: ForwardRefExoticComponent<
RadioProps & RefAttributes
@@ -1082,6 +1209,22 @@ export type Responsive = T | Partial>;
// @public (undocumented)
export function Row(props: RowProps): JSX_2.Element;
+// @public (undocumented)
+export interface RowConfig {
+ // (undocumented)
+ getHref?: (item: T) => string | undefined;
+ // (undocumented)
+ getIsDisabled?: (item: T) => boolean;
+ // (undocumented)
+ onClick?: (item: T) => void;
+}
+
+// @public (undocumented)
+export type RowRenderFn = (params: {
+ item: T;
+ index: number;
+}) => ReactNode;
+
// @public (undocumented)
export const SearchField: ForwardRefExoticComponent<
SearchFieldProps & RefAttributes
@@ -1112,6 +1255,14 @@ export interface SearchFieldProps
startCollapsed?: boolean;
}
+// @public (undocumented)
+export interface SearchState {
+ // (undocumented)
+ onSearchChange: (value: string) => void;
+ // (undocumented)
+ value: string;
+}
+
// @public (undocumented)
export const Select: ForwardRefExoticComponent<
SelectProps<'multiple' | 'single'> & RefAttributes
@@ -1171,6 +1322,17 @@ export interface SkeletonProps extends ComponentProps<'div'> {
width?: number | string;
}
+// @public (undocumented)
+export type SortDescriptor = SortDescriptor_2;
+
+// @public (undocumented)
+export interface SortState {
+ // (undocumented)
+ descriptor: SortDescriptor | null;
+ // (undocumented)
+ onSortChange: (descriptor: SortDescriptor) => void;
+}
+
// @public (undocumented)
export type Space =
| '0.5'
@@ -1251,7 +1413,18 @@ export interface SwitchProps extends SwitchProps_2 {
export const Tab: (props: TabProps) => JSX_2.Element;
// @public (undocumented)
-export const Table: (props: TableProps) => JSX_2.Element;
+export function Table({
+ columnConfig,
+ data,
+ loading,
+ isStale,
+ error,
+ pagination,
+ sort,
+ rowConfig,
+ selection,
+ emptyState,
+}: TableProps): JSX_2.Element;
// @public (undocumented)
export const TableBody: (
@@ -1281,6 +1454,9 @@ export const TableDefinition: {
readonly headSelection: 'bui-TableHeadSelection';
readonly cellSelection: 'bui-TableCellSelection';
};
+ readonly dataAttributes: {
+ readonly stale: readonly [true, false];
+ };
};
// @public (undocumented)
@@ -1288,8 +1464,25 @@ export const TableHeader: (
props: TableHeaderProps,
) => JSX_2.Element;
+// @public (undocumented)
+export interface TableItem {
+ // (undocumented)
+ id: string | number;
+}
+
// @public
-export function TablePagination(props: TablePaginationProps): JSX_2.Element;
+export function TablePagination({
+ pageSize,
+ offset,
+ totalCount,
+ hasNextPage,
+ hasPreviousPage,
+ onNextPage,
+ onPreviousPage,
+ onPageSizeChange,
+ showPageSizeOptions,
+ getLabel,
+}: TablePaginationProps): JSX_2.Element;
// @public
export const TablePaginationDefinition: {
@@ -1302,26 +1495,79 @@ export const TablePaginationDefinition: {
};
// @public (undocumented)
-export interface TablePaginationProps
- extends React.HTMLAttributes {
+export interface TablePaginationProps {
+ // (undocumented)
+ getLabel?: (params: {
+ pageSize: number;
+ offset?: number;
+ totalCount?: number;
+ }) => string;
+ // (undocumented)
+ hasNextPage: boolean;
+ // (undocumented)
+ hasPreviousPage: boolean;
// (undocumented)
offset?: number;
// (undocumented)
- onNextPage?: () => void;
+ onNextPage: () => void;
// (undocumented)
- onPageSizeChange?: (pageSize: number) => void;
+ onPageSizeChange?: (size: number) => void;
// (undocumented)
- onPreviousPage?: () => void;
+ onPreviousPage: () => void;
// (undocumented)
- pageSize?: number;
- // (undocumented)
- rowCount?: number;
- // (undocumented)
- setOffset?: (offset: number) => void;
- // (undocumented)
- setPageSize?: (pageSize: number) => void;
+ pageSize: number;
// (undocumented)
showPageSizeOptions?: boolean;
+ // (undocumented)
+ totalCount?: number;
+}
+
+// @public (undocumented)
+export type TablePaginationType = NoPagination | PagePagination;
+
+// @public (undocumented)
+export interface TableProps {
+ // (undocumented)
+ columnConfig: readonly ColumnConfig[];
+ // (undocumented)
+ data: T[] | undefined;
+ // (undocumented)
+ emptyState?: ReactNode;
+ // (undocumented)
+ error?: Error;
+ // (undocumented)
+ isStale?: boolean;
+ // (undocumented)
+ loading?: boolean;
+ // (undocumented)
+ pagination: TablePaginationType;
+ // (undocumented)
+ rowConfig?: RowConfig | RowRenderFn;
+ // (undocumented)
+ selection?: TableSelection;
+ // (undocumented)
+ sort?: SortState;
+}
+
+// @public (undocumented)
+export const TableRoot: (props: TableRootProps) => JSX_2.Element;
+
+// @public (undocumented)
+export interface TableRootProps extends TableProps_2 {
+ // (undocumented)
+ stale?: boolean;
+}
+
+// @public (undocumented)
+export interface TableSelection {
+ // (undocumented)
+ behavior?: TableProps_2['selectionBehavior'];
+ // (undocumented)
+ mode?: TableProps_2['selectionMode'];
+ // (undocumented)
+ onSelectionChange?: TableProps_2['onSelectionChange'];
+ // (undocumented)
+ selected?: TableProps_2['selectedKeys'];
}
// @public
@@ -1535,48 +1781,69 @@ export const useBreakpoint: () => {
down: (key: Breakpoint) => boolean;
};
-// @public
-export function useTable(
- config?: UseTableConfig,
-): UseTableResult;
+// @public (undocumented)
+export function useTable(
+ options: UseTableOptions,
+): UseTableResult;
// @public (undocumented)
-export interface UseTableConfig {
- data?: T[];
- pagination?: UseTablePaginationConfig;
+export interface UseTableCompleteOptions
+ extends QueryOptions {
+ // (undocumented)
+ filterFn?: (data: T[], filter: TFilter) => T[];
+ // (undocumented)
+ getData: () => T[] | Promise;
+ // (undocumented)
+ mode: 'complete';
+ // (undocumented)
+ paginationOptions?: PaginationOptions;
+ // (undocumented)
+ searchFn?: (data: T[], search: string) => T[];
+ // (undocumented)
+ sortFn?: (data: T[], sort: SortDescriptor) => T[];
}
// @public (undocumented)
-export interface UseTablePagination {
- data?: T[];
- nextPage: () => void;
- offset: number;
- pageSize: number;
- paginationProps: TablePaginationProps;
- previousPage: () => void;
- setOffset: (offset: number) => void;
- setPageSize: (pageSize: number) => void;
+export interface UseTableCursorOptions
+ extends QueryOptions {
+ // (undocumented)
+ getData: (params: CursorParams) => Promise>;
+ // (undocumented)
+ mode: 'cursor';
+ // (undocumented)
+ paginationOptions?: Omit;
}
// @public (undocumented)
-export interface UseTablePaginationConfig {
- defaultOffset?: number;
- defaultPageSize?: number;
- offset?: number;
- onNextPage?: () => void;
- onOffsetChange?: (offset: number) => void;
- onPageSizeChange?: (pageSize: number) => void;
- onPreviousPage?: () => void;
- pageSize?: number;
- rowCount?: number;
- showPageSizeOptions?: boolean;
+export interface UseTableOffsetOptions
+ extends QueryOptions {
+ // (undocumented)
+ getData: (params: OffsetParams) => Promise>;
+ // (undocumented)
+ mode: 'offset';
+ // (undocumented)
+ paginationOptions?: PaginationOptions;
}
// @public (undocumented)
-export interface UseTableResult {
- data?: T[];
- pagination: UseTablePagination;
- paginationProps: TablePaginationProps;
+export type UseTableOptions =
+ | UseTableCompleteOptions
+ | UseTableOffsetOptions
+ | UseTableCursorOptions;
+
+// @public (undocumented)
+export interface UseTableResult {
+ // (undocumented)
+ filter: FilterState;
+ // (undocumented)
+ reload: () => void;
+ // (undocumented)
+ search: SearchState;
+ // (undocumented)
+ tableProps: Omit<
+ TableProps,
+ 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState'
+ >;
}
// @public (undocumented)
diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css
index 001d5b21d1..ea148ae893 100644
--- a/packages/ui/src/components/Table/Table.module.css
+++ b/packages/ui/src/components/Table/Table.module.css
@@ -22,6 +22,11 @@
caption-side: bottom;
border-collapse: collapse;
table-layout: fixed;
+ transition: opacity 0.2s ease-in-out;
+
+ &[data-stale='true'] {
+ opacity: 0.6;
+ }
}
.bui-TableHeader {
diff --git a/packages/ui/src/components/Table/Table.stories.tsx b/packages/ui/src/components/Table/Table.stories.tsx
deleted file mode 100644
index f7687af1c3..0000000000
--- a/packages/ui/src/components/Table/Table.stories.tsx
+++ /dev/null
@@ -1,910 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import preview from '../../../../../.storybook/preview';
-import { useState } from 'react';
-import type { StoryFn } from '@storybook/react-vite';
-import { type Selection } from 'react-aria-components';
-import {
- Table,
- TableHeader,
- Column,
- TableBody,
- Row,
- Cell,
- CellText,
- CellProfile,
- useTable,
-} from '.';
-import { RadioGroup, Radio } from '../RadioGroup';
-import { Flex } from '../Flex';
-import { MemoryRouter } from 'react-router-dom';
-import { data as data1Raw } from './mocked-data1';
-import { data as data2 } from './mocked-data2';
-import { data as data3 } from './mocked-data3';
-import { data as data4 } from './mocked-data4';
-import { RiCactusLine } from '@remixicon/react';
-import { TablePagination } from '../TablePagination';
-import { Text } from '../Text';
-
-const meta = preview.meta({
- title: 'Backstage UI/Table',
- decorators: [
- (Story: StoryFn) => (
-
-
-
- ),
- ],
-});
-
-// Added this fix to fix Chromatic timeout error. This bug is due to rerendering the table with too many rows.
-// Work in progress to fix it here - https://github.com/backstage/backstage/pull/30687
-const data1 = data1Raw.slice(0, 10);
-
-export const TableOnly = meta.story({
- render: () => {
- return (
-
-
- Name
- Owner
- Type
- Lifecycle
-
-
- {data1.map(item => (
-
- }
- description={item.description}
- />
-
-
-
-
- ))}
-
-
- );
- },
-});
-
-export const WithPaginationUncontrolled = meta.story({
- render: () => {
- const { data, paginationProps } = useTable({ data: data1 });
-
- return (
- <>
-
-
- Name
- Owner
- Type
- Lifecycle
-
-
- {data?.map(item => (
-
- }
- description={item.description}
- />
-
-
-
-
- ))}
-
-
-
- >
- );
- },
-});
-
-export const WithPaginationControlled = meta.story({
- render: () => {
- const [offset, setOffset] = useState(0);
- const [pageSize, setPageSize] = useState(5);
-
- const { data, paginationProps } = useTable({
- data: data4,
- pagination: {
- offset,
- pageSize,
- onOffsetChange: setOffset,
- onPageSizeChange: setPageSize,
- onNextPage: () => console.log('Next page analytics'),
- onPreviousPage: () => console.log('Previous page analytics'),
- },
- });
-
- return (
- <>
-
-
- Band name
- Genre
- Year formed
- Albums
-
-
- {data?.map(item => (
-
-
-
-
-
-
- ))}
-
-
-
-
- Current state: offset={offset}, pageSize={pageSize}
-
- >
- );
- },
-});
-
-export const Sorting = meta.story({
- render: () => {
- return (
-
-
-
- Name
-
- Owner
- Type
- Lifecycle
-
-
- {data1.map(item => (
-
- }
- description={item.description}
- />
-
-
-
-
- ))}
-
-
- );
- },
-});
-
-export const TableRockBand = meta.story({
- render: () => {
- const { data, paginationProps } = useTable({
- data: data4,
- pagination: {
- defaultPageSize: 5,
- },
- });
-
- return (
- <>
-
-
- Band name
- Genre
- Year formed
- Albums
-
-
- {data?.map(item => (
-
-
-
-
-
-
- ))}
-
-
-
- >
- );
- },
-});
-
-export const RowClick = meta.story({
- render: () => {
- const { data, paginationProps } = useTable({
- data: data4,
- pagination: {
- defaultPageSize: 5,
- },
- });
-
- return (
- <>
-
-
- Band name
- Genre
- Year formed
- Albums
-
-
- {data?.map(item => (
- alert('Row clicked')}>
-
-
-
-
-
- ))}
-
-
-
- >
- );
- },
-});
-
-export const RowLink = meta.story({
- render: () => {
- const { data, paginationProps } = useTable({
- data: data4,
- pagination: {
- defaultPageSize: 5,
- },
- });
-
- return (
- <>
-
-
- Band name
- Genre
- Year formed
- Albums
-
-
- {data?.map(item => (
-
-
-
-
-
-
- ))}
-
-
-
- >
- );
- },
-});
-
-export const CellComponent = meta.story({
- name: 'Cell',
- render: () => {
- return (
-
-
- Name
-
-
-
- | Hello world |
-
-
- |
- This is a very long text that demonstrates how the Cell component
- handles lengthy content. It should wrap appropriately and maintain
- proper styling even when the text extends beyond the normal cell
- width. This helps ensure that the table remains readable and
- visually consistent regardless of the content length.
- |
-
-
- | Hello world |
-
-
-
- );
- },
-});
-
-export const CellTextComponent = meta.story({
- name: 'CellText',
- render: () => {
- return (
-
-
- Name
-
-
- {data2.map(item => (
-
-
-
- ))}
-
-
- );
- },
-});
-
-export const CellProfileComponent = meta.story({
- name: 'CellProfile',
- render: () => {
- return (
-
-
- Name
-
-
- {data3.map(item => (
-
-
-
- ))}
-
-
- );
- },
-});
-
-export const SelectionSingleToggle = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionMultiToggle = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionSingleReplace = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionMultiReplace = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionToggleWithActions = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
- alert(`Opening ${key}`)}
- >
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionReplaceWithActions = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
- alert(`Opening ${key}`)}
- >
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionToggleWithLinks = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionReplaceWithLinks = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionWithDisabledRows = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- },
-});
-
-export const SelectionWithPagination = meta.story({
- render: () => {
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- const { data, paginationProps } = useTable({
- data: data1,
- pagination: {
- defaultPageSize: 5,
- },
- });
-
- return (
- <>
-
-
- Name
- Owner
- Type
-
-
- {data?.map(item => (
-
-
-
-
-
- ))}
-
-
-
- >
- );
- },
-});
-
-export const SelectionModePlayground = meta.story({
- render: () => {
- const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
- 'multiple',
- );
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Selection mode:
-
- {
- setSelectionMode(value as 'single' | 'multiple');
- setSelectedKeys(new Set([]));
- }}
- >
- single
- multiple
-
-
-
- );
- },
-});
-
-export const SelectionBehaviorPlayground = meta.story({
- render: () => {
- const [selectionBehavior, setSelectionBehavior] = useState<
- 'toggle' | 'replace'
- >('toggle');
- const [selectedKeys, setSelectedKeys] = useState(new Set([]));
-
- return (
-
-
-
- Name
- Owner
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Selection behavior:
-
- {
- setSelectionBehavior(value as 'toggle' | 'replace');
- setSelectedKeys(new Set([]));
- }}
- >
- toggle
- replace
-
-
-
- );
- },
-});
diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx
index ffd0ab231d..59020dc2b0 100644
--- a/packages/ui/src/components/Table/components/Table.tsx
+++ b/packages/ui/src/components/Table/components/Table.tsx
@@ -14,24 +14,199 @@
* limitations under the License.
*/
-import { useStyles } from '../../../hooks/useStyles';
-import { TableDefinition } from '../definition';
-import {
- Table as ReactAriaTable,
- type TableProps,
-} from 'react-aria-components';
-import styles from '../Table.module.css';
-import clsx from 'clsx';
+import type { Key } from 'react-aria-components';
+import { TableRoot } from './TableRoot';
+import { TableHeader } from './TableHeader';
+import { TableBody } from './TableBody';
+import { Row } from './Row';
+import { Column } from './Column';
+import { TablePagination } from '../../TablePagination';
+import type {
+ TableProps,
+ TableItem,
+ RowConfig,
+ RowRenderFn,
+ TablePaginationType,
+} from '../types';
+import { Fragment, useId, useMemo } from 'react';
+import { VisuallyHidden } from '../../VisuallyHidden';
+import { Flex } from '../../Flex';
+
+function isRowRenderFn(
+ rowConfig: RowConfig | RowRenderFn | undefined,
+): rowConfig is RowRenderFn {
+ return typeof rowConfig === 'function';
+}
+
+function useDisabledRows({
+ data,
+ rowConfig,
+}: Pick, 'data' | 'rowConfig'>): Set | undefined {
+ return useMemo(() => {
+ if (!data || typeof rowConfig === 'function' || !rowConfig?.getIsDisabled) {
+ return;
+ }
+
+ return data.reduce>((set, item) => {
+ const isDisabled = rowConfig.getIsDisabled?.(item);
+ if (isDisabled) {
+ set.add(String(item.id));
+ }
+ return set;
+ }, new Set());
+ }, [data, rowConfig]);
+}
+
+function useLiveRegionLabel(
+ pagination: TablePaginationType,
+ isStale: boolean,
+ hasData: boolean,
+): string {
+ if (!hasData || pagination.type === 'none') {
+ return '';
+ }
+
+ const { pageSize, offset, totalCount, getLabel } = pagination;
+
+ if (isStale) {
+ return 'Loading table data.';
+ }
+
+ let liveRegionLabel = 'Table page loaded. ';
+
+ if (getLabel) {
+ liveRegionLabel += getLabel({ pageSize, offset, totalCount });
+ } else if (offset !== undefined) {
+ const fromCount = offset + 1;
+ const toCount = Math.min(offset + pageSize, totalCount ?? 0);
+ liveRegionLabel += `Showing ${fromCount} to ${toCount} of ${totalCount}`;
+ }
+ return liveRegionLabel;
+}
/** @public */
-export const Table = (props: TableProps) => {
- const { classNames, cleanedProps } = useStyles(TableDefinition, props);
+export function Table({
+ columnConfig,
+ data,
+ loading = false,
+ isStale = false,
+ error,
+ pagination,
+ sort,
+ rowConfig,
+ selection,
+ emptyState,
+}: TableProps) {
+ const liveRegionId = useId();
+
+ const visibleColumns = useMemo(
+ () => columnConfig.filter(col => !col.isHidden),
+ [columnConfig],
+ );
+ const disabledRows = useDisabledRows({ data, rowConfig });
+
+ const {
+ mode: selectionMode,
+ selected: selectedKeys,
+ behavior: selectionBehavior,
+ onSelectionChange,
+ } = selection || {};
+
+ if (loading && !data) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return Error: {error.message}
;
+ }
+
+ const liveRegionLabel = useLiveRegionLabel(
+ pagination,
+ isStale,
+ data !== undefined,
+ );
return (
-
+
+
+ {liveRegionLabel}
+
+
+
+
+ {column =>
+ column.header ? (
+ <>{column.header()}>
+ ) : (
+
+ {column.label}
+
+ )
+ }
+
+ {emptyState} : undefined
+ }
+ >
+ {item => {
+ const itemIndex = data?.indexOf(item) ?? -1;
+
+ if (isRowRenderFn(rowConfig)) {
+ return rowConfig({
+ item,
+ index: itemIndex,
+ });
+ }
+
+ return (
+ rowConfig?.onClick?.(item)
+ : undefined
+ }
+ >
+ {column => (
+ {column.cell(item)}
+ )}
+
+ );
+ }}
+
+
+ {pagination.type === 'page' && (
+
+ )}
+
);
-};
+}
diff --git a/packages/ui/src/components/Table/components/TableRoot.tsx b/packages/ui/src/components/Table/components/TableRoot.tsx
new file mode 100644
index 0000000000..95e80dcf51
--- /dev/null
+++ b/packages/ui/src/components/Table/components/TableRoot.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useStyles } from '../../../hooks/useStyles';
+import { TableDefinition } from '../definition';
+import { Table as ReactAriaTable } from 'react-aria-components';
+import styles from '../Table.module.css';
+import clsx from 'clsx';
+import { TableRootProps } from '../types';
+
+/** @public */
+export const TableRoot = (props: TableRootProps) => {
+ const { classNames, dataAttributes, cleanedProps } = useStyles(
+ TableDefinition,
+ props,
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/ui/src/components/Table/definition.ts b/packages/ui/src/components/Table/definition.ts
index 313604902c..ca4fca999a 100644
--- a/packages/ui/src/components/Table/definition.ts
+++ b/packages/ui/src/components/Table/definition.ts
@@ -42,4 +42,7 @@ export const TableDefinition = {
headSelection: 'bui-TableHeadSelection',
cellSelection: 'bui-TableCellSelection',
},
+ dataAttributes: {
+ stale: [true, false] as const,
+ },
} as const satisfies ComponentDefinition;
diff --git a/packages/ui/src/components/Table/hooks/types.ts b/packages/ui/src/components/Table/hooks/types.ts
index 8c93d678fc..f6311b7ebe 100644
--- a/packages/ui/src/components/Table/hooks/types.ts
+++ b/packages/ui/src/components/Table/hooks/types.ts
@@ -15,73 +15,144 @@
*/
import type { TablePaginationProps } from '../../TablePagination/types';
+import type { SortDescriptor, TableItem, TableProps } from '../types';
/** @public */
-export interface UseTablePaginationConfig {
- /** Total number of rows in the dataset - only needed when data is not provided at the top level */
- rowCount?: number;
+export interface FilterState {
+ value: TFilter | undefined;
+ onFilterChange: (filter: TFilter) => void;
+}
- // Controlled pagination with offset/pageSize (Backstage style)
- /** Current offset. When provided, pagination is controlled */
- offset?: number;
- /** Current page size. When provided, pagination is controlled */
+/** @public */
+export interface SearchState {
+ value: string;
+ onSearchChange: (value: string) => void;
+}
+
+/** @public */
+export interface QueryOptions {
+ initialSort?: SortDescriptor;
+ sort?: SortDescriptor | null;
+ onSortChange?: (sort: SortDescriptor) => void;
+
+ initialFilter?: TFilter;
+ filter?: TFilter;
+ onFilterChange?: (filter: TFilter) => void;
+
+ initialSearch?: string;
+ search?: string;
+ onSearchChange?: (search: string) => void;
+}
+
+/** @public */
+export interface PaginationOptions {
pageSize?: number;
- /** Callback when offset changes */
- onOffsetChange?: (offset: number) => void;
- /** Callback when page size changes */
- onPageSizeChange?: (pageSize: number) => void;
-
- // Uncontrolled pagination defaults
- /** Default page size for uncontrolled mode */
- defaultPageSize?: number;
- /** Default offset for uncontrolled mode */
- defaultOffset?: number;
-
- // Analytics callbacks
- /** Callback when next page is clicked */
- onNextPage?: () => void;
- /** Callback when previous page is clicked */
- onPreviousPage?: () => void;
-
- // UI options
- /** Whether to show page size options */
+ initialOffset?: number;
showPageSizeOptions?: boolean;
+ getLabel?: TablePaginationProps['getLabel'];
}
/** @public */
-export interface UseTablePagination {
- /** Props to pass to TablePagination component */
- paginationProps: TablePaginationProps;
- /** Current offset */
+export interface OffsetParams {
offset: number;
- /** Current page size */
pageSize: number;
- /** Sliced data for current page - only available when data is provided to useTable */
- data?: T[];
- /** Go to next page */
- nextPage: () => void;
- /** Go to previous page */
- previousPage: () => void;
- /** Set specific offset */
- setOffset: (offset: number) => void;
- /** Set page size */
- setPageSize: (pageSize: number) => void;
+ sort: SortDescriptor | null;
+ filter: TFilter | undefined;
+ search: string;
+ signal: AbortSignal;
}
/** @public */
-export interface UseTableConfig {
- /** Full dataset - when provided, rowCount is calculated automatically and sliced data is returned */
- data?: T[];
- /** Pagination configuration */
- pagination?: UseTablePaginationConfig;
+export interface CursorParams {
+ cursor: string | undefined;
+ pageSize: number;
+ sort: SortDescriptor | null;
+ filter: TFilter | undefined;
+ search: string;
+ signal: AbortSignal;
}
/** @public */
-export interface UseTableResult {
- /** Sliced data for current page */
- data?: T[];
- /** Props to pass to TablePagination component */
- paginationProps: TablePaginationProps;
- /** Pagination utilities */
- pagination: UseTablePagination;
+export interface OffsetResponse {
+ data: T[];
+ totalCount: number;
+}
+
+/** @public */
+export interface CursorResponse {
+ data: T[];
+ nextCursor?: string;
+ prevCursor?: string;
+ totalCount?: number;
+}
+
+/** @public */
+export interface UseTableCompleteOptions
+ extends QueryOptions {
+ mode: 'complete';
+ getData: () => T[] | Promise;
+ paginationOptions?: PaginationOptions;
+ sortFn?: (data: T[], sort: SortDescriptor) => T[];
+ filterFn?: (data: T[], filter: TFilter) => T[];
+ searchFn?: (data: T[], search: string) => T[];
+}
+
+/** @public */
+export interface UseTableOffsetOptions
+ extends QueryOptions {
+ mode: 'offset';
+ getData: (params: OffsetParams) => Promise>;
+ paginationOptions?: PaginationOptions;
+}
+
+/** @public */
+export interface UseTableCursorOptions
+ extends QueryOptions {
+ mode: 'cursor';
+ getData: (params: CursorParams) => Promise>;
+ paginationOptions?: Omit;
+}
+
+/** @public */
+export type UseTableOptions =
+ | UseTableCompleteOptions
+ | UseTableOffsetOptions
+ | UseTableCursorOptions;
+
+/** @public */
+export interface UseTableResult {
+ tableProps: Omit<
+ TableProps,
+ 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState'
+ >;
+ reload: () => void;
+ filter: FilterState;
+ search: SearchState;
+}
+
+/** @internal */
+export interface PaginationResult {
+ data: T[] | undefined;
+ loading: boolean;
+ error: Error | undefined;
+ totalCount: number | undefined;
+ offset?: number;
+ pageSize: number;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ onNextPage: () => void;
+ onPreviousPage: () => void;
+ onPageSizeChange: (size: number) => void;
+}
+
+/** @internal */
+export interface QueryState {
+ sort: SortDescriptor | null;
+ setSort: (sort: SortDescriptor) => void;
+
+ filter: TFilter | undefined;
+ setFilter: (filter: TFilter) => void;
+
+ search: string;
+ setSearch: (search: string) => void;
}
diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts
new file mode 100644
index 0000000000..3f199335d5
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
+import type { TableItem } from '../types';
+import type {
+ PaginationResult,
+ QueryState,
+ UseTableCompleteOptions,
+} from './types';
+import { useStableCallback } from './useStableCallback';
+
+/** @internal */
+export function useCompletePagination(
+ options: UseTableCompleteOptions,
+ query: QueryState,
+): PaginationResult & { reload: () => void } {
+ const {
+ getData: getDataProp,
+ paginationOptions = {},
+ sortFn,
+ filterFn,
+ searchFn,
+ } = options;
+ const { pageSize: defaultPageSize = 20, initialOffset = 0 } =
+ paginationOptions;
+
+ const getData = useStableCallback(getDataProp);
+ const { sort, filter, search } = query;
+
+ const [items, setItems] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(undefined);
+ const [loadCount, setLoadCount] = useState(0);
+
+ const [offset, setOffset] = useState(initialOffset);
+ const [pageSize, setPageSize] = useState(defaultPageSize);
+
+ // Load data on mount and when loadCount changes (reload trigger)
+ useEffect(() => {
+ let cancelled = false;
+ setIsLoading(true);
+ setError(undefined);
+
+ (async () => {
+ try {
+ const result = getData();
+ const data = result instanceof Promise ? await result : result;
+ if (!cancelled) {
+ setItems(data);
+ setIsLoading(false);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err : new Error(String(err)));
+ setIsLoading(false);
+ }
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [getData, loadCount]);
+
+ // Reset offset when query changes (query object is memoized)
+ const prevQueryRef = useRef(query);
+ useEffect(() => {
+ if (prevQueryRef.current !== query) {
+ prevQueryRef.current = query;
+ setOffset(0);
+ }
+ }, [query]);
+
+ // Process data client-side (filter, search, sort)
+ const processedData = useMemo(() => {
+ let result = [...items];
+ if (filter !== undefined && filterFn) {
+ result = filterFn(result, filter);
+ }
+ if (search && searchFn) {
+ result = searchFn(result, search);
+ }
+ if (sort && sortFn) {
+ result = sortFn(result, sort);
+ }
+ return result;
+ }, [items, sort, filter, search, filterFn, searchFn, sortFn]);
+
+ const totalCount = processedData.length;
+
+ // Paginate the processed data
+ const paginatedData = useMemo(
+ () => processedData.slice(offset, offset + pageSize),
+ [processedData, offset, pageSize],
+ );
+
+ const hasNextPage = offset + pageSize < totalCount;
+ const hasPreviousPage = offset > 0;
+
+ const onNextPage = useCallback(() => {
+ if (offset + pageSize < totalCount) {
+ setOffset(offset + pageSize);
+ }
+ }, [offset, pageSize, totalCount]);
+
+ const onPreviousPage = useCallback(() => {
+ if (offset > 0) {
+ setOffset(Math.max(0, offset - pageSize));
+ }
+ }, [offset, pageSize]);
+
+ const onPageSizeChange = useCallback((newSize: number) => {
+ setPageSize(newSize);
+ setOffset(0);
+ }, []);
+
+ const reload = useCallback(() => {
+ setOffset(0);
+ setLoadCount(c => c + 1);
+ }, []);
+
+ return {
+ data: paginatedData,
+ loading: isLoading,
+ error,
+ totalCount,
+ offset,
+ pageSize,
+ hasNextPage,
+ hasPreviousPage,
+ onNextPage,
+ onPreviousPage,
+ onPageSizeChange,
+ reload,
+ };
+}
diff --git a/packages/ui/src/components/Table/hooks/useCursorPagination.ts b/packages/ui/src/components/Table/hooks/useCursorPagination.ts
new file mode 100644
index 0000000000..007f39023f
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useCursorPagination.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useState, useCallback } from 'react';
+import type { TableItem } from '../types';
+import type {
+ UseTableCursorOptions,
+ CursorParams,
+ QueryState,
+ PaginationResult,
+} from './types';
+import { usePageCache } from './usePageCache';
+import { useStableCallback } from './useStableCallback';
+import { useDebouncedReload } from './useDebouncedReload';
+
+export function useCursorPagination(
+ options: UseTableCursorOptions,
+ query: QueryState,
+): PaginationResult & { reload: () => void } {
+ const { getData: getDataProp, paginationOptions = {} } = options;
+ const { pageSize: defaultPageSize = 20 } = paginationOptions;
+
+ const getData = useStableCallback(getDataProp);
+ const { sort, filter, search } = query;
+
+ const [pageSize, setPageSize] = useState(defaultPageSize);
+
+ const wrappedGetData = useCallback(
+ async ({
+ cursor,
+ signal,
+ }: {
+ cursor: string | undefined;
+ signal: AbortSignal;
+ }) => {
+ const params: CursorParams = {
+ cursor,
+ pageSize,
+ sort,
+ filter,
+ search,
+ signal,
+ };
+
+ const response = await getData(params);
+
+ return {
+ data: response.data,
+ prevCursor: response.prevCursor,
+ nextCursor: response.nextCursor,
+ totalCount: response.totalCount,
+ };
+ },
+ [getData, pageSize, sort, filter, search],
+ );
+
+ const cache = usePageCache({ getData: wrappedGetData });
+
+ useDebouncedReload(query, pageSize, cache.reload);
+
+ const onPageSizeChange = useCallback(
+ (newSize: number) => setPageSize(newSize),
+ [],
+ );
+
+ return {
+ data: cache.data,
+ loading: cache.loading,
+ error: cache.error,
+ totalCount: cache.totalCount,
+ offset: undefined,
+ pageSize,
+ hasNextPage: cache.hasNextPage,
+ hasPreviousPage: cache.hasPreviousPage,
+ onNextPage: cache.onNextPage,
+ onPreviousPage: cache.onPreviousPage,
+ onPageSizeChange,
+ reload: cache.reload,
+ };
+}
diff --git a/packages/ui/src/components/Table/hooks/useDebouncedReload.ts b/packages/ui/src/components/Table/hooks/useDebouncedReload.ts
new file mode 100644
index 0000000000..d90afa8861
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useDebouncedReload.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useEffect, useRef } from 'react';
+import type { QueryState } from './types';
+
+/**
+ * Triggers a debounced reload when query or pageSize changes.
+ * Debouncing reduces backend load during rapid changes (e.g., typing in search).
+ */
+/** @internal */
+export function useDebouncedReload(
+ query: QueryState,
+ pageSize: number,
+ reload: () => void,
+ delay: number = 200,
+): void {
+ const prevDepsRef = useRef({ query, pageSize });
+
+ useEffect(() => {
+ const prev = prevDepsRef.current;
+ if (prev.query !== query || prev.pageSize !== pageSize) {
+ prevDepsRef.current = { query, pageSize };
+ const timer = setTimeout(reload, delay);
+ return () => clearTimeout(timer);
+ }
+ return undefined;
+ }, [query, pageSize, reload, delay]);
+}
diff --git a/packages/ui/src/components/Table/hooks/useOffsetPagination.ts b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts
new file mode 100644
index 0000000000..c5844ec018
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useState, useCallback } from 'react';
+import type { TableItem } from '../types';
+import type {
+ UseTableOffsetOptions,
+ OffsetParams,
+ QueryState,
+ PaginationResult,
+} from './types';
+import { usePageCache } from './usePageCache';
+import { useStableCallback } from './useStableCallback';
+import { useDebouncedReload } from './useDebouncedReload';
+
+export function useOffsetPagination(
+ options: UseTableOffsetOptions,
+ query: QueryState,
+): PaginationResult & { reload: () => void } {
+ const { getData: getDataProp, paginationOptions = {} } = options;
+ const { pageSize: defaultPageSize = 20, initialOffset = 0 } =
+ paginationOptions;
+
+ const getData = useStableCallback(getDataProp);
+ const { sort, filter, search } = query;
+
+ const [pageSize, setPageSize] = useState(defaultPageSize);
+
+ const wrappedGetData = useCallback(
+ async ({
+ cursor,
+ signal,
+ }: {
+ cursor: number | undefined;
+ signal: AbortSignal;
+ }) => {
+ const currentOffset = cursor ?? 0;
+
+ const params: OffsetParams = {
+ offset: currentOffset,
+ pageSize,
+ sort,
+ filter,
+ search,
+ signal,
+ };
+
+ const response = await getData(params);
+
+ const prevCursor =
+ currentOffset > 0 ? Math.max(0, currentOffset - pageSize) : undefined;
+ const nextCursor =
+ currentOffset + pageSize < response.totalCount
+ ? currentOffset + pageSize
+ : undefined;
+
+ return {
+ data: response.data,
+ prevCursor,
+ nextCursor,
+ totalCount: response.totalCount,
+ };
+ },
+ [getData, pageSize, sort, filter, search],
+ );
+
+ const cache = usePageCache({
+ getData: wrappedGetData,
+ initialCurrentCursor: initialOffset > 0 ? initialOffset : undefined,
+ });
+
+ useDebouncedReload(query, pageSize, cache.reload);
+
+ const onPageSizeChange = useCallback(
+ (newSize: number) => setPageSize(newSize),
+ [],
+ );
+
+ return {
+ data: cache.data,
+ loading: cache.loading,
+ error: cache.error,
+ totalCount: cache.totalCount,
+ offset: cache.currentCursor ?? 0,
+ pageSize,
+ hasNextPage: cache.hasNextPage,
+ hasPreviousPage: cache.hasPreviousPage,
+ onNextPage: cache.onNextPage,
+ onPreviousPage: cache.onPreviousPage,
+ onPageSizeChange,
+ reload: cache.reload,
+ };
+}
diff --git a/packages/ui/src/components/Table/hooks/usePageCache.ts b/packages/ui/src/components/Table/hooks/usePageCache.ts
new file mode 100644
index 0000000000..9bf864749b
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/usePageCache.ts
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState, useCallback, useRef, useEffect } from 'react';
+
+const FIRST_PAGE_CURSOR = Symbol('firstPage');
+
+type CursorType = string | number;
+
+type InternalCursor =
+ | TCursor
+ | typeof FIRST_PAGE_CURSOR;
+
+interface PageEntry {
+ data: T[] | undefined;
+ nextCursor: InternalCursor | undefined;
+ prevCursor: InternalCursor | undefined;
+}
+
+interface GetDataResult {
+ data: T[];
+ nextCursor?: TCursor;
+ prevCursor?: TCursor;
+ totalCount?: number;
+}
+
+/** @internal */
+export interface UsePageCacheOptions {
+ getData: (params: {
+ cursor: TCursor | undefined;
+ signal: AbortSignal;
+ }) => Promise>;
+ initialCurrentCursor?: TCursor;
+}
+
+/** @internal */
+export interface UsePageCacheResult {
+ loading: boolean;
+ error: Error | undefined;
+ data: T[] | undefined;
+ totalCount: number | undefined;
+ currentCursor: TCursor | undefined;
+ hasPreviousPage: boolean;
+ onPreviousPage: () => void;
+ hasNextPage: boolean;
+ onNextPage: () => void;
+ reload: (options?: { keepCurrentCursor?: boolean }) => void;
+}
+
+type Direction = 'mount' | 'reset' | 'refresh' | 'next' | 'prev';
+
+class PageCacheStore {
+ private cache = new Map, PageEntry>();
+
+ get(cursor: InternalCursor): PageEntry | undefined {
+ return this.cache.get(cursor);
+ }
+
+ getOrCreate(cursor: InternalCursor): PageEntry {
+ const existing = this.cache.get(cursor);
+ if (existing) {
+ return existing;
+ }
+ const entry: PageEntry = {
+ data: undefined,
+ nextCursor: undefined,
+ prevCursor: undefined,
+ };
+ this.cache.set(cursor, entry);
+ return entry;
+ }
+
+ clear() {
+ this.cache.clear();
+ }
+
+ getTargetCursor(
+ direction: Direction,
+ currentCursor: InternalCursor,
+ initialCurrentCursor: TCursor | undefined,
+ ): InternalCursor | undefined {
+ if (direction === 'mount') {
+ return toInternalCursor(initialCurrentCursor);
+ }
+ if (direction === 'reset') {
+ return FIRST_PAGE_CURSOR;
+ }
+ if (direction === 'refresh') {
+ return currentCursor;
+ }
+ const currentEntry = this.cache.get(currentCursor);
+
+ if (!currentEntry) {
+ return;
+ }
+
+ return direction === 'next'
+ ? currentEntry.nextCursor
+ : currentEntry.prevCursor;
+ }
+
+ linkEntryToSource(
+ entry: PageEntry,
+ direction: Direction,
+ currentCursor: InternalCursor,
+ ) {
+ if (direction === 'next') {
+ entry.prevCursor = currentCursor;
+ } else if (direction === 'prev') {
+ entry.nextCursor = currentCursor;
+ }
+ }
+}
+
+function toInternalCursor(
+ cursor: TCursor | undefined,
+): InternalCursor {
+ return cursor === undefined ? FIRST_PAGE_CURSOR : cursor;
+}
+
+function toExternalCursor(
+ cursor: InternalCursor,
+): TCursor | undefined {
+ return cursor === FIRST_PAGE_CURSOR ? undefined : cursor;
+}
+
+/** @internal */
+export function usePageCache(
+ options: UsePageCacheOptions,
+): UsePageCacheResult {
+ const { getData, initialCurrentCursor } = options;
+
+ const [currentCursor, setCurrentCursor] = useState>(
+ () => toInternalCursor(initialCurrentCursor),
+ );
+
+ const cacheStore = useRef(new PageCacheStore()).current;
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(undefined);
+ const [totalCount, setTotalCount] = useState(undefined);
+
+ const abortControllerRef = useRef(null);
+
+ const currentPage = cacheStore.get(currentCursor);
+ const data = currentPage?.data;
+ const hasNextPage = currentPage?.nextCursor !== undefined;
+ const hasPreviousPage = currentPage?.prevCursor !== undefined;
+
+ const goToPage = useCallback(
+ async (direction: Direction) => {
+ const targetCursor = cacheStore.getTargetCursor(
+ direction,
+ currentCursor,
+ initialCurrentCursor,
+ );
+
+ if (!targetCursor) {
+ return;
+ }
+
+ const existingEntry = cacheStore.get(targetCursor);
+ if (existingEntry?.data !== undefined) {
+ setCurrentCursor(targetCursor);
+ return;
+ }
+
+ const entry = cacheStore.getOrCreate(targetCursor);
+ cacheStore.linkEntryToSource(entry, direction, currentCursor);
+ setCurrentCursor(targetCursor);
+
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
+
+ setLoading(true);
+ setError(undefined);
+
+ try {
+ const result = await getData({
+ cursor: toExternalCursor(targetCursor),
+ signal: abortController.signal,
+ });
+
+ if (abortController.signal.aborted) {
+ return;
+ }
+
+ entry.data = result.data;
+
+ if (entry.nextCursor === undefined && result.nextCursor !== undefined) {
+ entry.nextCursor = result.nextCursor;
+ }
+ if (entry.prevCursor === undefined && result.prevCursor !== undefined) {
+ entry.prevCursor = result.prevCursor;
+ }
+
+ if (result.totalCount !== undefined) {
+ setTotalCount(result.totalCount);
+ }
+
+ setLoading(false);
+ } catch (err) {
+ if (abortController.signal.aborted) {
+ return;
+ }
+
+ setError(err instanceof Error ? err : new Error(String(err)));
+ setLoading(false);
+ }
+ },
+ [getData, initialCurrentCursor, currentCursor, cacheStore],
+ );
+
+ useEffect(() => {
+ goToPage('mount');
+
+ return () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ };
+ }, []);
+
+ const onNextPage = useCallback(() => {
+ if (loading) return;
+ const page = cacheStore.get(currentCursor);
+ if (!page?.nextCursor) return;
+ goToPage('next');
+ }, [loading, currentCursor, goToPage, cacheStore]);
+
+ const onPreviousPage = useCallback(() => {
+ if (loading) return;
+ const page = cacheStore.get(currentCursor);
+ if (!page?.prevCursor) return;
+ goToPage('prev');
+ }, [loading, currentCursor, goToPage, cacheStore]);
+
+ const reload = useCallback(
+ (reloadOptions?: { keepCurrentCursor?: boolean }) => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ cacheStore.clear();
+
+ goToPage(reloadOptions?.keepCurrentCursor ? 'refresh' : 'reset');
+ },
+ [goToPage, cacheStore],
+ );
+
+ return {
+ loading,
+ error,
+ data,
+ totalCount,
+ currentCursor: toExternalCursor(currentCursor),
+ hasPreviousPage,
+ onPreviousPage,
+ hasNextPage,
+ onNextPage,
+ reload,
+ };
+}
diff --git a/packages/ui/src/components/Table/hooks/useQueryState.ts b/packages/ui/src/components/Table/hooks/useQueryState.ts
new file mode 100644
index 0000000000..308d9fd385
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useQueryState.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useMemo, useState, useCallback } from 'react';
+import type { QueryOptions, QueryState } from './types';
+
+function useControlledStateHelper(
+ initialValue: TInitial,
+ controlledValue: TControlled | undefined,
+ onChange: ((value: T) => void) | undefined,
+) {
+ const [internalValue, setInternalValue] = useState(initialValue);
+
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
+
+ const setValue = useCallback(
+ (newValue: T) => {
+ if (controlledValue === undefined) {
+ setInternalValue(newValue as unknown as TInitial);
+ }
+ if (onChange) {
+ onChange(newValue);
+ }
+ },
+ [controlledValue, onChange],
+ );
+
+ return [value, setValue] as const;
+}
+
+/** @internal */
+export function useQueryState(
+ options: QueryOptions,
+): QueryState {
+ const [sort, setSort] = useControlledStateHelper(
+ options.initialSort ?? null,
+ options.sort,
+ options.onSortChange,
+ );
+ const [filter, setFilter] = useControlledStateHelper(
+ options.initialFilter,
+ options.filter,
+ options.onFilterChange,
+ );
+ const [search, setSearch] = useControlledStateHelper(
+ options.initialSearch ?? '',
+ options.search,
+ options.onSearchChange,
+ );
+
+ return useMemo(
+ () => ({ sort, setSort, filter, setFilter, search, setSearch }),
+ [sort, setSort, filter, setFilter, search, setSearch],
+ );
+}
diff --git a/packages/ui/src/components/Table/hooks/useStableCallback.ts b/packages/ui/src/components/Table/hooks/useStableCallback.ts
new file mode 100644
index 0000000000..1764e07dd0
--- /dev/null
+++ b/packages/ui/src/components/Table/hooks/useStableCallback.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useRef, useCallback } from 'react';
+
+/**
+ * Returns a stable callback reference that always calls the latest version
+ * of the provided function. Useful for callbacks passed as props that may
+ * change on every render but shouldn't trigger effect re-runs.
+ *
+ * @internal
+ */
+export function useStableCallback any>(fn: T): T {
+ const ref = useRef(fn);
+ ref.current = fn;
+
+ return useCallback((...args: Parameters) => ref.current(...args), []) as T;
+}
diff --git a/packages/ui/src/components/Table/hooks/useTable.ts b/packages/ui/src/components/Table/hooks/useTable.ts
index cd1ddd5c9c..e024f8a335 100644
--- a/packages/ui/src/components/Table/hooks/useTable.ts
+++ b/packages/ui/src/components/Table/hooks/useTable.ts
@@ -13,154 +13,119 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import { useState, useMemo, useCallback } from 'react';
-import type { TablePaginationProps } from '../../TablePagination/types';
+import { useMemo, useRef } from 'react';
+import type { SortState, TableItem, TableProps } from '../types';
import type {
- UseTableConfig,
+ PaginationOptions,
+ PaginationResult,
+ UseTableOptions,
UseTableResult,
- UseTablePagination,
} from './types';
+import { useQueryState } from './useQueryState';
+import { useCompletePagination } from './useCompletePagination';
+import { useCursorPagination } from './useCursorPagination';
+import { useOffsetPagination } from './useOffsetPagination';
-/**
- * Hook for managing table state including pagination and future features like sorting.
- * Supports both controlled and uncontrolled modes using offset/pageSize pattern (Backstage style).
- *
- * @public
- */
-export function useTable(
- config: UseTableConfig = {},
-): UseTableResult {
- const { data, pagination: paginationConfig = {} } = config;
+function useTableProps(
+ paginationResult: PaginationResult,
+ sortState: SortState,
+ paginationOptions: PaginationOptions = {},
+): Omit<
+ TableProps,
+ 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState'
+> {
+ const { showPageSizeOptions = true, getLabel } = paginationOptions;
- const {
- rowCount: providedRowCount,
- offset: controlledOffset,
- pageSize: controlledPageSize,
- onOffsetChange,
- onPageSizeChange,
- defaultPageSize = 10,
- defaultOffset = 0,
- onNextPage,
- onPreviousPage,
- showPageSizeOptions = true,
- } = paginationConfig;
+ const previousDataRef = useRef(paginationResult.data);
+ if (paginationResult.data) {
+ previousDataRef.current = paginationResult.data;
+ }
- // Determine if we're in controlled mode
- const isControlled =
- controlledOffset !== undefined || controlledPageSize !== undefined;
+ const displayData = paginationResult.data ?? previousDataRef.current;
+ const isStale = paginationResult.loading && displayData !== undefined;
- // Use providedRowCount if passed, otherwise fallback to data length
- const rowCount = providedRowCount ?? data?.length ?? 0;
-
- // Internal state for uncontrolled mode
- const [internalOffset, setInternalOffset] = useState(defaultOffset);
- const [internalPageSize, setInternalPageSize] = useState(defaultPageSize);
-
- // Calculate current values
- const currentOffset = controlledOffset ?? internalOffset;
- const currentPageSize = controlledPageSize ?? internalPageSize;
-
- // Calculate sliced data if data array is provided
- const currentData = useMemo(() => {
- if (!data) return undefined;
- return data.slice(currentOffset, currentOffset + currentPageSize);
- }, [data, currentOffset, currentPageSize]);
-
- // Update functions
- const setOffset = useCallback(
- (newOffset: number) => {
- if (isControlled) {
- onOffsetChange?.(newOffset);
- } else {
- setInternalOffset(newOffset);
- }
- },
- [isControlled, onOffsetChange],
- );
-
- const setPageSize = useCallback(
- (newPageSize: number) => {
- // When changing page size, reset to first page to avoid showing empty results
- const newOffset = 0;
-
- if (isControlled) {
- onPageSizeChange?.(newPageSize);
- onOffsetChange?.(newOffset);
- } else {
- setInternalPageSize(newPageSize);
- setInternalOffset(newOffset);
- }
- },
- [isControlled, onPageSizeChange, onOffsetChange],
- );
-
- const nextPage = useCallback(() => {
- const nextOffset = currentOffset + currentPageSize;
- if (nextOffset < rowCount) {
- onNextPage?.();
- setOffset(nextOffset);
- }
- }, [currentOffset, currentPageSize, rowCount, onNextPage, setOffset]);
-
- const previousPage = useCallback(() => {
- if (currentOffset > 0) {
- onPreviousPage?.();
- const prevOffset = Math.max(0, currentOffset - currentPageSize);
- setOffset(prevOffset);
- }
- }, [currentOffset, currentPageSize, onPreviousPage, setOffset]);
-
- // Pagination props for TablePagination component
- const paginationProps: TablePaginationProps = useMemo(
+ const pagination = useMemo(
() => ({
- offset: currentOffset,
- pageSize: currentPageSize,
- rowCount,
- setOffset,
- setPageSize,
- onNextPage,
- onPreviousPage,
+ type: 'page' as const,
+ pageSize: paginationResult.pageSize,
+ offset: paginationResult.offset,
+ totalCount: paginationResult.totalCount,
+ hasNextPage: paginationResult.hasNextPage,
+ hasPreviousPage: paginationResult.hasPreviousPage,
+ onNextPage: paginationResult.onNextPage,
+ onPreviousPage: paginationResult.onPreviousPage,
+ onPageSizeChange: paginationResult.onPageSizeChange,
showPageSizeOptions,
+ getLabel,
}),
[
- currentOffset,
- currentPageSize,
- rowCount,
- setOffset,
- setPageSize,
- onNextPage,
- onPreviousPage,
- showPageSizeOptions,
+ paginationResult.pageSize,
+ paginationResult.offset,
+ paginationResult.totalCount,
+ paginationResult.hasNextPage,
+ paginationResult.hasPreviousPage,
+ paginationResult.onNextPage,
+ paginationResult.onPreviousPage,
+ paginationResult.onPageSizeChange,
],
);
- const pagination: UseTablePagination = useMemo(
+ return useMemo(
() => ({
- paginationProps,
- offset: currentOffset,
- pageSize: currentPageSize,
- data: currentData,
- nextPage,
- previousPage,
- setOffset,
- setPageSize,
+ data: displayData,
+ loading: paginationResult.loading,
+ isStale,
+ error: paginationResult.error,
+ pagination,
+ sort: sortState,
}),
[
- paginationProps,
- currentOffset,
- currentPageSize,
- currentData,
- nextPage,
- previousPage,
- setOffset,
- setPageSize,
+ displayData,
+ paginationResult.loading,
+ isStale,
+ paginationResult.error,
+ pagination,
+ showPageSizeOptions,
+ getLabel,
+ sortState,
],
);
+}
+
+/** @public */
+export function useTable(
+ options: UseTableOptions,
+): UseTableResult {
+ const query = useQueryState(options);
+
+ let pagination: PaginationResult & { reload: () => void };
+
+ // Conditional hooks - mode is stable for lifetime of component
+ if (options.mode === 'complete') {
+ pagination = useCompletePagination(options, query);
+ } else if (options.mode === 'offset') {
+ pagination = useOffsetPagination(options, query);
+ } else if (options.mode === 'cursor') {
+ pagination = useCursorPagination(options, query);
+ } else {
+ throw new Error('Invalid mode');
+ }
+
+ const sortState: SortState = useMemo(
+ () => ({ descriptor: query.sort, onSortChange: query.setSort }),
+ [query.sort, query.setSort],
+ );
+
+ const tableProps = useTableProps(
+ pagination,
+ sortState,
+ options.paginationOptions ?? {},
+ );
return {
- data: currentData,
- paginationProps,
- pagination,
+ tableProps,
+ reload: pagination.reload,
+ filter: { value: query.filter, onFilterChange: query.setFilter },
+ search: { value: query.search, onSearchChange: query.setSearch },
};
}
diff --git a/packages/ui/src/components/Table/index.ts b/packages/ui/src/components/Table/index.ts
index 42ba6ff9c9..0d3d5ff8eb 100644
--- a/packages/ui/src/components/Table/index.ts
+++ b/packages/ui/src/components/Table/index.ts
@@ -15,6 +15,7 @@
*/
export { Table } from './components/Table';
+export { TableRoot } from './components/TableRoot';
export { TableHeader } from './components/TableHeader';
export { TableBody } from './components/TableBody';
export { Column } from './components/Column';
@@ -29,12 +30,33 @@ export type {
CellTextProps,
CellProfileProps,
ColumnProps,
+ TableProps,
+ TableRootProps,
+ TableItem,
+ ColumnConfig,
+ RowConfig,
+ RowRenderFn,
+ TableSelection,
+ SortState,
+ SortDescriptor,
+ NoPagination,
+ PagePagination,
+ TablePaginationType,
} from './types';
export type {
- UseTableConfig,
+ UseTableOptions,
UseTableResult,
- UseTablePagination,
- UseTablePaginationConfig,
+ UseTableCompleteOptions,
+ UseTableOffsetOptions,
+ UseTableCursorOptions,
+ OffsetParams,
+ OffsetResponse,
+ CursorParams,
+ CursorResponse,
+ FilterState,
+ SearchState,
+ QueryOptions,
+ PaginationOptions,
} from './hooks/types';
export { TableDefinition } from './definition';
diff --git a/packages/ui/src/components/Table/stories/Table.dev.stories.tsx b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx
new file mode 100644
index 0000000000..5be055cd01
--- /dev/null
+++ b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx
@@ -0,0 +1,965 @@
+/* eslint-disable no-restricted-syntax */
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState, Fragment } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Table,
+ TableRoot,
+ TableHeader,
+ TableBody,
+ Column,
+ Row,
+ CellText,
+ CellProfile,
+ useTable,
+ type ColumnConfig,
+} from '..';
+import { Button } from '../../Button';
+import { TextField } from '../../TextField';
+import { Select } from '../../Select';
+import { Flex } from '../../Flex';
+import { data as data1 } from './mocked-data1';
+import { data as data4 } from './mocked-data4';
+import { selectionData, selectionColumns, tableStoriesMeta } from './utils';
+
+const meta = {
+ title: 'Backstage UI/Table/dev',
+ ...tableStoriesMeta,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+type Data1Item = (typeof data1)[0];
+type Data4Item = (typeof data4)[0];
+
+export const BasicLocalData: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return ;
+ },
+};
+
+export const Sorting: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ isSortable: true,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ isSortable: true,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ isSortable: true,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ cell: item => ,
+ isSortable: true,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ initialSort: { column: 'name', direction: 'ascending' },
+ sortFn: (items, { column, direction }) => {
+ return [...items].sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ if (column === 'name') {
+ aVal = a.name;
+ bVal = b.name;
+ } else if (column === 'owner') {
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ } else if (column === 'type') {
+ aVal = a.type;
+ bVal = b.type;
+ } else {
+ aVal = a.lifecycle;
+ bVal = b.lifecycle;
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return direction === 'descending' ? -cmp : cmp;
+ });
+ },
+ });
+
+ return ;
+ },
+};
+
+export const Search: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ isSortable: true,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps, search } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ searchFn: (items, query) => {
+ const lowerQuery = query.toLowerCase();
+ return items.filter(
+ item =>
+ item.name.toLowerCase().includes(lowerQuery) ||
+ item.owner.name.toLowerCase().includes(lowerQuery) ||
+ item.type.toLowerCase().includes(lowerQuery),
+ );
+ },
+ });
+
+ return (
+
+
search.onSearchChange(value)}
+ style={{ marginBottom: '16px' }}
+ />
+ No results found
+ ) : (
+ No data available
+ )
+ }
+ {...tableProps}
+ />
+
+ );
+ },
+};
+
+export const Selection: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+
+ );
+ },
+};
+
+export const RowLinks: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data4,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+ `/bands/${item.id}` }}
+ />
+ );
+ },
+};
+
+export const Reload: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps, reload } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+
+
reload()}>Refresh Data
+
+
+ );
+ },
+};
+
+export const ServerSidePaginationOffset: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'offset',
+ getData: async ({ offset, pageSize }) => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return {
+ data: data1.slice(offset, offset + pageSize),
+ totalCount: data1.length,
+ };
+ },
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return ;
+ },
+};
+
+export const ServerSidePaginationCursor: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'cursor',
+ getData: async ({ cursor, pageSize }) => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ const startIndex = cursor ? parseInt(cursor, 10) : 0;
+ const nextIndex = startIndex + pageSize;
+ return {
+ data: data4.slice(startIndex, nextIndex),
+ totalCount: data4.length,
+ nextCursor: nextIndex < data4.length ? String(nextIndex) : undefined,
+ prevCursor:
+ startIndex > 0
+ ? String(Math.max(0, startIndex - pageSize))
+ : undefined,
+ };
+ },
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return ;
+ },
+};
+
+export const CustomRowRender: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+ (
+
+ {column => (
+
+ {column.id === 'name' ? (
+
+ ) : (
+ column.cell(item)
+ )}
+
+ )}
+
+ )}
+ />
+ );
+ },
+};
+
+export const AtomicComponents: Story = {
+ render: () => {
+ const displayData = data1.slice(0, 5);
+
+ return (
+
+
+ Name
+ Owner
+ Type
+
+
+ {displayData.map(item => (
+
+
+
+
+
+ ))}
+
+
+ );
+ },
+};
+
+export const RowClick: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ cell: item => ,
+ },
+ {
+ id: 'albums',
+ label: 'Albums',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data4,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+ alert(`Clicked: ${item.name}`) }}
+ />
+ );
+ },
+};
+
+export const SelectionSingleToggle: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionMultiToggle: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionWithRowClick: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ alert(`Clicked: ${item.name}`) }}
+ />
+ );
+ },
+};
+
+export const SelectionWithRowLinks: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ `/items/${item.id}` }}
+ />
+ );
+ },
+};
+
+export const SelectionWithPagination: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data1,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionSingleReplace: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionMultiReplace: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionReplaceWithRowClick: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ alert(`Opening ${item.name}`) }}
+ />
+ );
+ },
+};
+
+export const SelectionReplaceWithRowLinks: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ `/items/${item.id}` }}
+ />
+ );
+ },
+};
+
+// Type filter interface for ComprehensiveServerSide story
+interface TypeFilter {
+ type: string | null;
+}
+
+/**
+ * Comprehensive example showcasing a common complex use case:
+ * - Server-side offset pagination
+ * - Search/filtering
+ * - Sorting
+ * - Multi-selection
+ * - Type filter dropdown
+ */
+export const ComprehensiveServerSide: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const typeOptions = [
+ { value: '', label: 'All types' },
+ { value: 'service', label: 'Service' },
+ { value: 'website', label: 'Website' },
+ { value: 'library', label: 'Library' },
+ { value: 'documentation', label: 'Documentation' },
+ { value: 'other', label: 'Other' },
+ ];
+
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ isSortable: true,
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps, search, filter } = useTable({
+ mode: 'offset',
+ initialSort: { column: 'name', direction: 'ascending' },
+ getData: async ({
+ offset,
+ pageSize,
+ sort,
+ filter: typeFilter,
+ search: searchQuery,
+ }) => {
+ // Simulate server-side filtering, sorting, and pagination
+ // with slower and slower responses
+ const page = Math.floor(offset / pageSize) + 1;
+ await new Promise(resolve => setTimeout(resolve, 300 * page));
+
+ let filtered = [...data1];
+
+ // Apply search filter
+ if (searchQuery) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ item =>
+ item.name.toLowerCase().includes(query) ||
+ item.owner.name.toLowerCase().includes(query) ||
+ item.description?.toLowerCase().includes(query),
+ );
+ }
+
+ // Apply type filter
+ if (typeFilter?.type) {
+ filtered = filtered.filter(item => item.type === typeFilter.type);
+ }
+
+ // Apply sorting
+ if (sort) {
+ filtered.sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ switch (sort.column) {
+ case 'owner':
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ break;
+ case 'type':
+ aVal = a.type;
+ bVal = b.type;
+ break;
+ case 'lifecycle':
+ aVal = a.lifecycle;
+ bVal = b.lifecycle;
+ break;
+ default:
+ aVal = a.name;
+ bVal = b.name;
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return sort.direction === 'descending' ? -cmp : cmp;
+ });
+ }
+
+ return {
+ data: filtered.slice(offset, offset + pageSize),
+ totalCount: filtered.length,
+ };
+ },
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+
+
+
+ filter.onFilterChange({ type: key === '' ? null : String(key) })
+ }
+ style={{ width: 180 }}
+ />
+
+ No results match your filters
+ ) : (
+ No data available
+ )
+ }
+ />
+
+ );
+ },
+};
diff --git a/packages/ui/src/components/Table/stories/Table.docs.stories.tsx b/packages/ui/src/components/Table/stories/Table.docs.stories.tsx
new file mode 100644
index 0000000000..49e0b44885
--- /dev/null
+++ b/packages/ui/src/components/Table/stories/Table.docs.stories.tsx
@@ -0,0 +1,198 @@
+/* eslint-disable no-restricted-syntax */
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { Table, CellText, CellProfile, useTable, type ColumnConfig } from '..';
+import { Flex } from '../../Flex';
+import { Text } from '../../Text';
+import { RadioGroup, Radio } from '../../RadioGroup';
+import { data as data4 } from './mocked-data4';
+import { selectionData, selectionColumns, tableStoriesMeta } from './utils';
+
+const meta = {
+ title: 'Backstage UI/Table/docs',
+ ...tableStoriesMeta,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+type Data4Item = (typeof data4)[0];
+
+export const TableRockBand: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ cell: item => ,
+ },
+ {
+ id: 'albums',
+ label: 'Albums',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data4,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return ;
+ },
+};
+
+export const SelectionToggleWithActions: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ alert(`Clicked: ${item.name}`) }}
+ />
+ );
+ },
+};
+
+export const SelectionModePlayground: Story = {
+ render: () => {
+ const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
+ 'multiple',
+ );
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+
+
+
+ Selection mode:
+
+ {
+ setSelectionMode(value as 'single' | 'multiple');
+ setSelected(new Set());
+ }}
+ >
+ single
+ multiple
+
+
+
+ );
+ },
+};
+
+export const SelectionBehaviorPlayground: Story = {
+ render: () => {
+ const [selectionBehavior, setSelectionBehavior] = useState<
+ 'toggle' | 'replace'
+ >('toggle');
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+
+
+
+
+ Selection behavior:
+
+ {
+ setSelectionBehavior(value as 'toggle' | 'replace');
+ setSelected(new Set());
+ }}
+ >
+ toggle
+ replace
+
+
+
+ );
+ },
+};
diff --git a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx
new file mode 100644
index 0000000000..34259dde96
--- /dev/null
+++ b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx
@@ -0,0 +1,340 @@
+/* eslint-disable no-restricted-syntax */
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { Table, CellText, CellProfile, useTable, type ColumnConfig } from '..';
+import { data as data1 } from './mocked-data1';
+import { data as data4 } from './mocked-data4';
+import { selectionData, selectionColumns, tableStoriesMeta } from './utils';
+
+const meta = {
+ title: 'Backstage UI/Table/visual',
+ ...tableStoriesMeta,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+type Data1Item = (typeof data1)[0];
+type Data4Item = (typeof data4)[0];
+type CellTextVariantsItem = (typeof cellTextVariantsData)[0];
+
+export const ProfileCells: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ cell: item => ,
+ },
+ {
+ id: 'albums',
+ label: 'Albums',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data4,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return ;
+ },
+};
+
+export const EmptyState: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => [],
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+ No data available}
+ />
+ );
+ },
+};
+
+export const NoPagination: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ );
+ },
+};
+
+export const SelectionWithDisabledRows: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => selectionData,
+ paginationOptions: { pageSize: 10 },
+ });
+
+ return (
+ item.id === 2,
+ }}
+ />
+ );
+ },
+};
+
+// Data for CellTextVariants story showcasing multiple features
+const cellTextVariantsData = [
+ {
+ id: 1,
+ name: 'Authentication Service',
+ description: 'Handles user login and session management',
+ type: 'service',
+ owner: 'Platform Team',
+ },
+ {
+ id: 2,
+ name: 'A very long component name that should be truncated when it exceeds the available column width',
+ description:
+ 'This is also a very long description that demonstrates text truncation behavior in the table cells',
+ type: 'library',
+ owner: 'Frontend Team',
+ },
+ {
+ id: 3,
+ name: 'API Gateway',
+ description: 'Routes and validates API requests',
+ type: 'service',
+ owner: 'Backend Team',
+ },
+];
+
+export const CellTextVariants: Story = {
+ render: () => {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(['1', '3']),
+ );
+ const [sortDescriptor, setSortDescriptor] = useState<{
+ column: string;
+ direction: 'ascending' | 'descending';
+ }>({ column: 'name', direction: 'ascending' });
+
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => (
+ 📦}
+ />
+ ),
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ setSortDescriptor({
+ column: String(descriptor.column),
+ direction: descriptor.direction,
+ }),
+ }}
+ />
+ );
+ },
+};
+
+export const LoadingState: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ );
+ },
+};
+
+export const ErrorState: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ );
+ },
+};
+
+export const StaleState: Story = {
+ render: () => {
+ const columns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ cell: item => ,
+ },
+ ];
+
+ return (
+
+ );
+ },
+};
diff --git a/packages/ui/src/components/Table/mocked-data1.ts b/packages/ui/src/components/Table/stories/mocked-data1.ts
similarity index 93%
rename from packages/ui/src/components/Table/mocked-data1.ts
rename to packages/ui/src/components/Table/stories/mocked-data1.ts
index ea5a9f2db8..b508bce387 100644
--- a/packages/ui/src/components/Table/mocked-data1.ts
+++ b/packages/ui/src/components/Table/stories/mocked-data1.ts
@@ -15,6 +15,7 @@
*/
export interface DataProps {
+ id: string;
name: string;
owner: {
name: string;
@@ -29,6 +30,7 @@ export interface DataProps {
export const data: DataProps[] = [
{
+ id: 'authentication-and-authorization-service',
name: 'authentication-and-authorization-service',
owner: {
name: 'security-team',
@@ -42,6 +44,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-interface-dashboard-and-analytics-platform',
name: 'user-interface-dashboard-and-analytics-platform',
owner: {
name: 'frontend-team',
@@ -55,6 +58,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'payment-gateway',
name: 'payment-gateway',
owner: {
name: 'finance-team',
@@ -68,6 +72,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'real-time-analytics-processing-and-visualization-engine',
name: 'real-time-analytics-processing-and-visualization-engine',
owner: {
name: 'data-team',
@@ -81,6 +86,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'notification-center',
name: 'notification-center',
owner: {
name: 'platform-team',
@@ -94,6 +100,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'administrative-control-panel-and-user-management-interface',
name: 'administrative-control-panel-and-user-management-interface',
owner: {
name: 'frontend-team',
@@ -107,6 +114,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'search-indexer',
name: 'search-indexer',
owner: {
name: 'search-team',
@@ -120,6 +128,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'cross-platform-mobile-application-framework',
name: 'cross-platform-mobile-application-framework',
owner: {
name: 'mobile-team',
@@ -133,6 +142,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'database-migration',
name: 'database-migration',
owner: {
name: 'devops-team',
@@ -146,6 +156,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'api-gateway',
name: 'api-gateway',
owner: {
name: 'platform-team',
@@ -159,6 +170,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'content-management',
name: 'content-management',
owner: {
name: 'content-team',
@@ -172,6 +184,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'enterprise-reporting-and-analytics-dashboard',
name: 'enterprise-reporting-and-analytics-dashboard',
owner: {
name: 'analytics-team',
@@ -185,6 +198,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'image-processing-and-optimization-service',
name: 'image-processing-and-optimization-service',
owner: {
name: 'media-team',
@@ -198,6 +212,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'customer-portal',
name: 'customer-portal',
owner: {
name: 'frontend-team',
@@ -211,6 +226,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'log-aggregator',
name: 'log-aggregator',
owner: {
name: 'devops-team',
@@ -224,6 +240,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'identity-provider',
name: 'identity-provider',
owner: {
name: 'security-team',
@@ -237,6 +254,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'document-storage',
name: 'document-storage',
owner: {
name: 'storage-team',
@@ -250,6 +268,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'workflow-engine',
name: 'workflow-engine',
owner: {
name: 'platform-team',
@@ -263,6 +282,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'mobile-backend',
name: 'mobile-backend',
owner: {
name: 'mobile-team',
@@ -276,6 +296,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'system-monitoring-and-alerting-dashboard',
name: 'system-monitoring-and-alerting-dashboard',
owner: {
name: 'devops-team',
@@ -289,6 +310,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'email-service',
name: 'email-service',
owner: {
name: 'communication-team',
@@ -302,6 +324,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-pipeline',
name: 'data-pipeline',
owner: {
name: 'data-team',
@@ -315,6 +338,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'configuration-manager',
name: 'configuration-manager',
owner: {
name: 'platform-team',
@@ -328,6 +352,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'testing-framework',
name: 'testing-framework',
owner: {
name: 'qa-team',
@@ -341,6 +366,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'cache-service',
name: 'cache-service',
owner: {
name: 'platform-team',
@@ -354,6 +380,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'billing-system',
name: 'billing-system',
owner: {
name: 'finance-team',
@@ -367,6 +394,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'comprehensive-product-documentation-and-api-reference',
name: 'comprehensive-product-documentation-and-api-reference',
owner: {
name: 'docs-team',
@@ -380,6 +408,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'queue-manager',
name: 'queue-manager',
owner: {
name: 'platform-team',
@@ -393,6 +422,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'security-scanner',
name: 'security-scanner',
owner: {
name: 'security-team',
@@ -406,6 +436,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'user-profile',
name: 'user-profile',
owner: {
name: 'frontend-team',
@@ -419,6 +450,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-warehouse',
name: 'data-warehouse',
owner: {
name: 'data-team',
@@ -432,6 +464,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'deployment-automation',
name: 'deployment-automation',
owner: {
name: 'devops-team',
@@ -445,6 +478,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'chat-service',
name: 'chat-service',
owner: {
name: 'communication-team',
@@ -458,6 +492,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'analytics-dashboard',
name: 'analytics-dashboard',
owner: {
name: 'analytics-team',
@@ -471,6 +506,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'file-uploader',
name: 'file-uploader',
owner: {
name: 'storage-team',
@@ -484,6 +520,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'search-service',
name: 'search-service',
owner: {
name: 'search-team',
@@ -497,6 +534,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'mobile-sdk',
name: 'mobile-sdk',
owner: {
name: 'mobile-team',
@@ -510,6 +548,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'performance-monitor',
name: 'performance-monitor',
owner: {
name: 'devops-team',
@@ -523,6 +562,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'content-delivery',
name: 'content-delivery',
owner: {
name: 'media-team',
@@ -536,6 +576,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-authentication',
name: 'user-authentication',
owner: {
name: 'security-team',
@@ -549,6 +590,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-export',
name: 'data-export',
owner: {
name: 'data-team',
@@ -562,6 +604,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'admin-api',
name: 'admin-api',
owner: {
name: 'platform-team',
@@ -575,6 +618,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'testing-dashboard',
name: 'testing-dashboard',
owner: {
name: 'qa-team',
@@ -587,6 +631,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'message-broker',
name: 'message-broker',
owner: {
name: 'platform-team',
@@ -600,6 +645,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'payment-processor',
name: 'payment-processor',
owner: {
name: 'finance-team',
@@ -613,6 +659,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'document-viewer',
name: 'document-viewer',
owner: {
name: 'frontend-team',
@@ -625,6 +672,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'load-balancer',
name: 'load-balancer',
owner: {
name: 'devops-team',
@@ -638,6 +686,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'security-audit',
name: 'security-audit',
owner: {
name: 'security-team',
@@ -651,6 +700,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-settings',
name: 'user-settings',
owner: {
name: 'frontend-team',
@@ -664,6 +714,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-import',
name: 'data-import',
owner: {
name: 'data-team',
@@ -677,6 +728,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'infrastructure-monitor',
name: 'infrastructure-monitor',
owner: {
name: 'devops-team',
@@ -690,6 +742,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'notification-manager',
name: 'notification-manager',
owner: {
name: 'communication-team',
@@ -703,6 +756,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'analytics-processor',
name: 'analytics-processor',
owner: {
name: 'analytics-team',
@@ -716,6 +770,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'file-manager',
name: 'file-manager',
owner: {
name: 'storage-team',
@@ -728,6 +783,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'search-index',
name: 'search-index',
owner: {
name: 'search-team',
@@ -740,6 +796,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'mobile-authentication',
name: 'mobile-authentication',
owner: {
name: 'mobile-team',
@@ -753,6 +810,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'system-monitor',
name: 'system-monitor',
owner: {
name: 'devops-team',
@@ -766,6 +824,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'media-processor',
name: 'media-processor',
owner: {
name: 'media-team',
@@ -778,6 +837,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-management',
name: 'user-management',
owner: {
name: 'security-team',
@@ -790,6 +850,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-transformer',
name: 'data-transformer',
owner: {
name: 'data-team',
@@ -803,6 +864,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'admin-dashboard',
name: 'admin-dashboard',
owner: {
name: 'platform-team',
@@ -816,6 +878,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'test-automation',
name: 'test-automation',
owner: {
name: 'qa-team',
@@ -828,6 +891,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'event-bus',
name: 'event-bus',
owner: {
name: 'platform-team',
@@ -840,6 +904,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'invoice-generator',
name: 'invoice-generator',
owner: {
name: 'finance-team',
@@ -852,6 +917,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'document-editor',
name: 'document-editor',
owner: {
name: 'frontend-team',
@@ -864,6 +930,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'service-discovery',
name: 'service-discovery',
owner: {
name: 'devops-team',
@@ -876,6 +943,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'security-monitor',
name: 'security-monitor',
owner: {
name: 'security-team',
@@ -888,6 +956,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-preferences',
name: 'user-preferences',
owner: {
name: 'frontend-team',
@@ -900,6 +969,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-validator',
name: 'data-validator',
owner: {
name: 'data-team',
@@ -912,6 +982,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'infrastructure-automation',
name: 'infrastructure-automation',
owner: {
name: 'devops-team',
@@ -925,6 +996,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'notification-dispatcher',
name: 'notification-dispatcher',
owner: {
name: 'communication-team',
@@ -938,6 +1010,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'analytics-collector',
name: 'analytics-collector',
owner: {
name: 'analytics-team',
@@ -950,6 +1023,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'file-processor',
name: 'file-processor',
owner: {
name: 'storage-team',
@@ -962,6 +1036,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'search-analyzer',
name: 'search-analyzer',
owner: {
name: 'search-team',
@@ -974,6 +1049,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'mobile-notifications',
name: 'mobile-notifications',
owner: {
name: 'mobile-team',
@@ -986,6 +1062,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'system-alerts',
name: 'system-alerts',
owner: {
name: 'devops-team',
@@ -998,6 +1075,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'media-encoder',
name: 'media-encoder',
owner: {
name: 'media-team',
@@ -1010,6 +1088,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-authorization',
name: 'user-authorization',
owner: {
name: 'security-team',
@@ -1022,6 +1101,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-aggregator',
name: 'data-aggregator',
owner: {
name: 'data-team',
@@ -1034,6 +1114,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'admin-authentication',
name: 'admin-authentication',
owner: {
name: 'platform-team',
@@ -1046,6 +1127,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'test-coverage',
name: 'test-coverage',
owner: {
name: 'qa-team',
@@ -1058,6 +1140,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'event-processor',
name: 'event-processor',
owner: {
name: 'platform-team',
@@ -1070,6 +1153,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'payment-validator',
name: 'payment-validator',
owner: {
name: 'finance-team',
@@ -1082,6 +1166,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'document-converter',
name: 'document-converter',
owner: {
name: 'frontend-team',
@@ -1094,6 +1179,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'service-health',
name: 'service-health',
owner: {
name: 'devops-team',
@@ -1106,6 +1192,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'security-logger',
name: 'security-logger',
owner: {
name: 'security-team',
@@ -1118,6 +1205,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-analytics',
name: 'user-analytics',
owner: {
name: 'frontend-team',
@@ -1131,6 +1219,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'data-cleaner',
name: 'data-cleaner',
owner: {
name: 'data-team',
@@ -1143,6 +1232,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'infrastructure-deployer',
name: 'infrastructure-deployer',
owner: {
name: 'devops-team',
@@ -1155,6 +1245,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'notification-queue',
name: 'notification-queue',
owner: {
name: 'communication-team',
@@ -1167,6 +1258,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'analytics-exporter',
name: 'analytics-exporter',
owner: {
name: 'analytics-team',
@@ -1179,6 +1271,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'file-validator',
name: 'file-validator',
owner: {
name: 'storage-team',
@@ -1191,6 +1284,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'search-optimizer',
name: 'search-optimizer',
owner: {
name: 'search-team',
@@ -1203,6 +1297,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'mobile-analytics',
name: 'mobile-analytics',
owner: {
name: 'mobile-team',
@@ -1215,6 +1310,7 @@ export const data: DataProps[] = [
lifecycle: 'experimental',
},
{
+ id: 'system-logger',
name: 'system-logger',
owner: {
name: 'devops-team',
@@ -1227,6 +1323,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'media-validator',
name: 'media-validator',
owner: {
name: 'media-team',
@@ -1239,6 +1336,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'user-audit',
name: 'user-audit',
owner: {
name: 'security-team',
@@ -1251,6 +1349,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'data-normalizer',
name: 'data-normalizer',
owner: {
name: 'data-team',
@@ -1263,6 +1362,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'admin-authorization',
name: 'admin-authorization',
owner: {
name: 'platform-team',
@@ -1275,6 +1375,7 @@ export const data: DataProps[] = [
lifecycle: 'production',
},
{
+ id: 'test-reporting',
name: 'test-reporting',
owner: {
name: 'qa-team',
diff --git a/packages/ui/src/components/Table/mocked-data2.ts b/packages/ui/src/components/Table/stories/mocked-data2.ts
similarity index 100%
rename from packages/ui/src/components/Table/mocked-data2.ts
rename to packages/ui/src/components/Table/stories/mocked-data2.ts
diff --git a/packages/ui/src/components/Table/mocked-data3.ts b/packages/ui/src/components/Table/stories/mocked-data3.ts
similarity index 100%
rename from packages/ui/src/components/Table/mocked-data3.ts
rename to packages/ui/src/components/Table/stories/mocked-data3.ts
diff --git a/packages/ui/src/components/Table/mocked-data4.ts b/packages/ui/src/components/Table/stories/mocked-data4.ts
similarity index 99%
rename from packages/ui/src/components/Table/mocked-data4.ts
rename to packages/ui/src/components/Table/stories/mocked-data4.ts
index b86d4c903f..5cedd2cbae 100644
--- a/packages/ui/src/components/Table/mocked-data4.ts
+++ b/packages/ui/src/components/Table/stories/mocked-data4.ts
@@ -15,6 +15,7 @@
*/
export interface RockBandDataProps {
+ id: string;
name: string;
image: string;
description: string;
@@ -31,6 +32,7 @@ export interface RockBandDataProps {
export const data: RockBandDataProps[] = [
{
+ id: 'The Beatles',
name: 'The Beatles',
image:
'https://upload.wikimedia.org/wikipedia/en/thumb/4/42/Beatles_-_Abbey_Road.jpg/250px-Beatles_-_Abbey_Road.jpg',
@@ -57,6 +59,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.thebeatles.com',
},
{
+ id: 'Led Zeppelin',
name: 'Led Zeppelin',
image:
'data:image/jpeg;base64,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',
@@ -74,6 +77,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.ledzeppelin.com',
},
{
+ id: 'Pink Floyd',
name: 'Pink Floyd',
image:
'https://upload.wikimedia.org/wikipedia/en/d/d6/Pink_Floyd_-_all_members.jpg',
@@ -97,6 +101,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.pinkfloyd.com',
},
{
+ id: 'The Rolling Stones',
name: 'The Rolling Stones',
image:
'data:image/jpeg;base64,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',
@@ -119,6 +124,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.rollingstones.com',
},
{
+ id: 'Queen',
name: 'Queen',
image: 'https://i.scdn.co/image/af2b8e57f6d7b5d43a616bd1e27ba552cd8bfd42',
description:
@@ -134,6 +140,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.queenonline.com',
},
{
+ id: 'The Who',
name: 'The Who',
image:
'https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcQoKRCDsHx1DyrnXBxOgpPUHZZIwebCq91Ge8oQylUJXlWzlOwtzQtepNBzfbT_497Ymm1sU1Oq2OV0qGmxa6igziOxHJX4Eo8oyk088RpF',
@@ -155,6 +162,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.thewho.com',
},
{
+ id: 'Black Sabbath',
name: 'Black Sabbath',
image:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qFdD0l8GNdMw30NkViSV7BNfUab-7wZXWs1jC3qYIt6llh-WVpJ9MLmgv4QKo9Gq-FOOYJ3-aOr9jQjDhcToYWj2VO9LbBIh7MO6e34PNw',
@@ -171,6 +179,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.blacksabbath.com',
},
{
+ id: 'The Doors',
name: 'The Doors',
image:
'https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcQVPqssHHK-u-EaYkVo8YuiCBINkJYVsAkqIlhojm2swhdc1Xn9lWkzokObi1_lAcr1CZ_dEbKnBTD9hfkUcPJ9vrwg-ARlgurH2rA5I4iwXQ',
@@ -188,6 +197,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.thedoors.com',
},
{
+ id: 'Jimi Hendrix Experience',
name: 'Jimi Hendrix Experience',
image:
'https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcQkDuNxy0xKisGP4lUk7N0QqrOQpyHNy9U2lpQhKBH5BqnRn6drBm8HYWVwcFfea1brVTxs7jSSVH1hjyrwTlzGRZCgfC91C-YgEStSzuvMwg',
@@ -205,6 +215,7 @@ export const data: RockBandDataProps[] = [
website: 'https://www.jimihendrix.com',
},
{
+ id: 'Cream',
name: 'Cream',
image:
'data:image/jpeg;base64,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',
diff --git a/packages/ui/src/components/Table/stories/utils.tsx b/packages/ui/src/components/Table/stories/utils.tsx
new file mode 100644
index 0000000000..45bc6341ad
--- /dev/null
+++ b/packages/ui/src/components/Table/stories/utils.tsx
@@ -0,0 +1,54 @@
+/* eslint-disable no-restricted-syntax */
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Meta } from '@storybook/react-vite';
+import { MemoryRouter } from 'react-router-dom';
+import { CellText, type ColumnConfig } from '..';
+
+// Selection demo data
+export const selectionData = [
+ { id: 1, name: 'Component Library', owner: 'Design System', type: 'library' },
+ { id: 2, name: 'API Gateway', owner: 'Platform', type: 'service' },
+ { id: 3, name: 'Documentation Site', owner: 'DevEx', type: 'website' },
+];
+
+// Selection demo columns
+export const selectionColumns: ColumnConfig<(typeof selectionData)[0]>[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+// Shared meta config for Table stories
+export const tableStoriesMeta = {
+ decorators: [
+ (Story: () => JSX.Element) => (
+
+
+
+ ),
+ ],
+} satisfies Partial ;
diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts
index 1ac477e3ff..5fec5355e5 100644
--- a/packages/ui/src/components/Table/types.ts
+++ b/packages/ui/src/components/Table/types.ts
@@ -16,9 +16,24 @@
import {
CellProps as ReactAriaCellProps,
- ColumnProps as AriaColumnProps,
+ ColumnProps as ReactAriaColumnProps,
+ TableProps as ReactAriaTableProps,
} from 'react-aria-components';
+import type { ReactNode } from 'react';
+import type { SortDescriptor as ReactStatelySortDescriptor } from 'react-stately';
import type { TextColors } from '../../types';
+import { TablePaginationProps } from '../TablePagination';
+
+/**
+ * @public
+ */
+export type SortDescriptor = ReactStatelySortDescriptor;
+
+/** @public */
+export interface SortState {
+ descriptor: SortDescriptor | null;
+ onSortChange: (descriptor: SortDescriptor) => void;
+}
/** @public */
export interface CellProps extends ReactAriaCellProps {}
@@ -42,6 +57,76 @@ export interface CellProfileProps extends ReactAriaCellProps {
}
/** @public */
-export interface ColumnProps extends Omit {
+export interface ColumnProps extends Omit {
children?: React.ReactNode;
}
+
+/** @public */
+export interface TableRootProps extends ReactAriaTableProps {
+ stale?: boolean;
+}
+
+/** @public */
+export interface TableItem {
+ id: string | number;
+}
+
+/** @public */
+export interface NoPagination {
+ type: 'none';
+}
+
+/** @public */
+export interface PagePagination extends TablePaginationProps {
+ type: 'page';
+}
+
+/** @public */
+export type TablePaginationType = NoPagination | PagePagination;
+
+/** @public */
+export interface ColumnConfig {
+ id: string;
+ label: string;
+ cell: (item: T) => ReactNode;
+ header?: () => ReactNode;
+ isSortable?: boolean;
+ isHidden?: boolean;
+ width?: number | string;
+ isRowHeader?: boolean;
+}
+
+/** @public */
+export interface RowConfig {
+ getHref?: (item: T) => string | undefined;
+ onClick?: (item: T) => void;
+ getIsDisabled?: (item: T) => boolean;
+}
+
+/** @public */
+export type RowRenderFn = (params: {
+ item: T;
+ index: number;
+}) => ReactNode;
+
+/** @public */
+export interface TableSelection {
+ mode?: ReactAriaTableProps['selectionMode'];
+ behavior?: ReactAriaTableProps['selectionBehavior'];
+ selected?: ReactAriaTableProps['selectedKeys'];
+ onSelectionChange?: ReactAriaTableProps['onSelectionChange'];
+}
+
+/** @public */
+export interface TableProps {
+ columnConfig: readonly ColumnConfig[];
+ data: T[] | undefined;
+ loading?: boolean;
+ isStale?: boolean;
+ error?: Error;
+ pagination: TablePaginationType;
+ sort?: SortState;
+ rowConfig?: RowConfig | RowRenderFn;
+ selection?: TableSelection;
+ emptyState?: ReactNode;
+}
diff --git a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx
index a7d949a31d..dd76f7c55a 100644
--- a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx
+++ b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 The Backstage Authors
+ * Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,41 +13,96 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import preview from '../../../../../.storybook/preview';
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
import { TablePagination } from './TablePagination';
-const meta = preview.meta({
+const noop = () => {};
+
+const meta = {
title: 'Backstage UI/TablePagination',
component: TablePagination,
argTypes: {
offset: { control: 'number' },
pageSize: { control: 'radio', options: [5, 10, 20, 30, 40, 50] },
- rowCount: { control: 'number' },
- showPageSizeOptions: { control: 'boolean', defaultValue: true },
- setOffset: { action: 'setOffset' },
- setPageSize: { action: 'setPageSize' },
+ totalCount: { control: 'number' },
+ hasNextPage: { control: 'boolean' },
+ hasPreviousPage: { control: 'boolean' },
+ showPageSizeOptions: { control: 'boolean' },
},
-});
+} satisfies Meta;
-export const Default = meta.story({
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
args: {
offset: 0,
pageSize: 10,
- rowCount: 100,
+ totalCount: 100,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ onNextPage: noop,
+ onPreviousPage: noop,
+ onPageSizeChange: noop,
+ showPageSizeOptions: true,
},
- render: args => {
- // const [{}, updateArgs] = useArgs();
+};
- return (
-