Simple table
A simple stateless table leveraging the columns
configuration.
Title | Time | Event Type | Status | Probability | Severity | Priority | |
---|---|---|---|---|---|---|---|
Event 1 | 2020-03-20 | Anomaly detection | Closed | 1% | Critical | High | |
Event 2 | 2020-03-20 | Anomaly detection | Open | 2% | Major | Medium | |
Event 3 | 2020-03-20 | Anomaly detection | Closed | 3% | Average | Low | |
Event 4 | 2020-03-20 | Anomaly detection | Open | 4% | Minor | High | |
Event 5 | 2020-03-20 | Anomaly detection | Closed | 5% | Critical | Medium | |
Event 6 | 2020-03-20 | Anomaly detection | Open | 6% | Major | Low | |
Event 7 | 2020-03-20 | Anomaly detection | Closed | 7% | Average | High | |
Event 8 | 2020-03-20 | Anomaly detection | Open | 8% | Minor | Medium |
import { useMemo, useState } from "react"; import { HvIconButton, HvTable, HvTableBody, HvTableCell, HvTableColumnConfig, HvTableContainer, HvTableHead, HvTableHeader, HvTableRow, HvTableSection, useHvTable, } from "@hitachivantara/uikit-react-core"; import { Delete } from "@hitachivantara/uikit-react-icons"; import { makeData, type AssetEvent } from "./makeData"; export default function Demo() { const [data] = useState(() => makeData(8)); const columns = useMemo<HvTableColumnConfig<AssetEvent>[]>( () => [ { Header: "Title", accessor: "name", style: { minWidth: 120 } }, { Header: "Time", accessor: "createdDate", style: { minWidth: 100 } }, { Header: "Event Type", accessor: "eventType", style: { minWidth: 100 } }, { Header: "Status", accessor: "status", style: { minWidth: 100 } }, { Header: "Probability", accessor: "riskScore", align: "right", Cell: ({ value }) => `${value}%`, }, { Header: "Severity", accessor: "severity" }, { Header: "Priority", accessor: "priority" }, { id: "delete", variant: "actions", Cell: () => ( <HvIconButton title="Delete"> <Delete /> </HvIconButton> ), }, ], [], ); return <MyTable data={data} columns={columns} />; } /** A simple generic client-side table. */ export const MyTable = <T extends object>(props: { columns: HvTableColumnConfig<T>[]; data: T[] | undefined; }) => { const { columns, data } = props; const table = useHvTable<T>({ columns, data }); return ( <HvTableSection> <HvTableContainer className="max-h-500px"> <HvTable {...table.getTableProps()}> <HvTableHead {...table.getTableHeadProps?.()}> {table.headerGroups.map((headerGroup) => ( <HvTableRow {...headerGroup.getHeaderGroupProps()} key={headerGroup.getHeaderGroupProps().key} > {headerGroup.headers.map((col) => ( <HvTableHeader {...col.getHeaderProps()} key={col.getHeaderProps().key} > {col.render("Header")} </HvTableHeader> ))} </HvTableRow> ))} </HvTableHead> <HvTableBody {...table.getTableBodyProps()}> {table.rows.map((row) => { table.prepareRow(row); return ( <HvTableRow {...row.getRowProps()} key={row.getRowProps().key}> {row.cells.map((cell) => ( <HvTableCell className="text-nowrap" {...cell.getCellProps()} key={cell.getCellProps().key} > {cell.render("Cell")} </HvTableCell> ))} </HvTableRow> ); })} </HvTableBody> </HvTable> </HvTableContainer> </HvTableSection> ); };
No data table
Custom content, like an HvEmptyState
, can be displayed inside table cells, by leveraging the native colSpan
feature.
Name | Age | Role | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No data to display. |
<HvTable> <HvTableHead> <HvTableRow> <HvTableHeader>Name</HvTableHeader> <HvTableHeader>Age</HvTableHeader> <HvTableHeader>Role</HvTableHeader> </HvTableRow> </HvTableHead> <HvTableBody> <HvTableRow> <HvTableCell colSpan={100} style={{ height: 96 }}> <HvEmptyState message="No data to display." icon={<Ban />} /> </HvTableCell> </HvTableRow> </HvTableBody> </HvTable>
Client-side table
A client-side table supporting the most commonly used table features: row sorting, selection, and actions, sticky header and columns, pagination, and bulk actions.
Title | Time | Event Type | Status | Probability | Severity | Priority | ||
---|---|---|---|---|---|---|---|---|
Event 1 | 2020-03-20 | Anomaly detection | Closed | 1% | Critical | High | ||
Event 2 | 2020-03-20 | Anomaly detection | Open | 2% | Major | Medium | ||
Event 3 | 2020-03-20 | Anomaly detection | Closed | 3% | Average | Low | ||
Event 4 | 2020-03-20 | Anomaly detection | Open | 4% | Minor | High | ||
Event 5 | 2020-03-20 | Anomaly detection | Closed | 5% | Critical | Medium | ||
Event 6 | 2020-03-20 | Anomaly detection | Open | 6% | Major | Low | ||
Event 7 | 2020-03-20 | Anomaly detection | Closed | 7% | Average | High | ||
Event 8 | 2020-03-20 | Anomaly detection | Open | 8% | Minor | Medium | ||
Event 9 | 2020-03-20 | Anomaly detection | Closed | 9% | Critical | Low | ||
Event 10 | 2020-03-20 | Anomaly detection | Open | 10% | Major | High |
Showrows
10
import { useCallback, useMemo, useState } from "react"; import { HvActionGeneric, HvBulkActions, HvEmptyState, HvIconButton, HvPagination, HvRowInstance, HvTable, HvTableBody, HvTableCell, HvTableColumnConfig, HvTableContainer, HvTableHead, HvTableHeader, HvTableInstance, HvTableOptions, HvTableRow, HvTableSection, HvTableState, useHvBulkActions, useHvPagination, useHvRowSelection, useHvSortBy, useHvTable, useHvTableSticky, } from "@hitachivantara/uikit-react-core"; import { Ban, Delete, Duplicate, Preview, } from "@hitachivantara/uikit-react-icons"; import { AssetEvent, makeData } from "./makeData"; export default function Demo() { const [data, setData] = useState(() => makeData(128)); const deleteRow = useCallback((row: HvRowInstance<AssetEvent>) => { setData((prev) => prev.filter((r) => r.id !== row.original.id)); }, []); const columns = useMemo<HvTableColumnConfig<AssetEvent>[]>( () => [ { Header: "Title", accessor: "name", style: { minWidth: 120 } }, { Header: "Time", accessor: "createdDate", style: { minWidth: 100 } }, { Header: "Event Type", accessor: "eventType", style: { minWidth: 100 } }, { Header: "Status", accessor: "status", style: { minWidth: 100 } }, { Header: "Probability", accessor: "riskScore", align: "right", Cell: ({ value }) => `${value}%`, }, { Header: "Severity", accessor: "severity" }, { Header: "Priority", accessor: "priority" }, { id: "delete", variant: "actions", Cell: ({ row }) => ( <HvIconButton title="Delete" onClick={() => deleteRow(row)}> <Delete /> </HvIconButton> ), }, ], [deleteRow], ); return ( <MyTable data={data} columns={columns} bulkActions={[ { id: "clone", label: "Clone", icon: <Duplicate /> }, { id: "remove", label: "Remove", icon: <Delete /> }, { id: "preview", label: "Preview", icon: <Preview />, disabled: true }, ]} onBulkAction={(evt, action, selectedRows) => { if (action.id === "remove") { selectedRows.forEach(deleteRow); } else if (action.id === "clone") { const clonedData = selectedRows.map(({ original }) => ({ ...original, id: `${original.id}-copy`, name: `${original.name}-copy`, })); setData((prev) => prev.concat(clonedData)); } }} options={{ // other `useHvTable` options getRowId: (row) => row.id, initialState: { pageSize: 10, }, }} /> ); } /** * A generic client-side table. * Includes row selection & sorting, bulk actions, pagination, sticky headers. */ export const MyTable = <T extends object>(props: { columns: HvTableColumnConfig<T>[]; data: T[] | undefined; initialState?: Partial<HvTableState<T>>; bulkActions?: HvActionGeneric[]; onBulkAction?: ( event: React.SyntheticEvent, action: HvActionGeneric, selectedRows: HvTableInstance<T>["selectedFlatRows"], ) => void; options?: HvTableOptions<T>; }) => { const { columns, data, bulkActions, onBulkAction, options } = props; const table = useHvTable<T>( { columns, data, stickyHeader: true, ...options, }, useHvTableSticky, useHvSortBy, useHvPagination, useHvRowSelection, useHvBulkActions, ); const renderTableRow = (i: number) => { const row = table.page[i]; if (!row) { // render up to 16 <EmptyRow> when there are multiple pages const showEmptyRow = table.pageCount && table.pageCount > 1 && i < 16; return showEmptyRow ? ( <HvTableRow key={`empty-${i}`}> <HvTableCell colSpan={100} /> </HvTableRow> ) : null; } table.prepareRow(row); return ( <HvTableRow {...row.getRowProps()} key={row.getRowProps().key}> {row.cells.map((cell) => ( <HvTableCell className="text-nowrap" {...cell.getCellProps()} key={cell.getCellProps().key} > {cell.render("Cell")} </HvTableCell> ))} </HvTableRow> ); }; return ( <HvTableSection> {table.page.length > 0 && ( <HvBulkActions actions={bulkActions} maxVisibleActions={1} onAction={(evt, action) => { onBulkAction?.(evt, action, table.selectedFlatRows); }} {...table.getHvBulkActionsProps?.()} /> )} <HvTableContainer className="max-h-500px"> <HvTable {...table.getTableProps()}> <HvTableHead {...table.getTableHeadProps?.()}> {table.headerGroups.map((headerGroup) => ( <HvTableRow {...headerGroup.getHeaderGroupProps()} key={headerGroup.getHeaderGroupProps().key} > {headerGroup.headers.map((col) => ( <HvTableHeader {...col.getHeaderProps()} key={col.getHeaderProps().key} > {col.render("Header")} </HvTableHeader> ))} </HvTableRow> ))} </HvTableHead> <HvTableBody {...table.getTableBodyProps()}> {table.page.length > 0 ? ( [...Array(table.state.pageSize).keys()].map(renderTableRow) ) : ( <HvTableRow> <HvTableCell colSpan={100} style={{ height: 96 }}> <HvEmptyState message="No data to display" icon={<Ban />} /> </HvTableCell> </HvTableRow> )} </HvTableBody> </HvTable> </HvTableContainer> {table.page.length > 0 && ( <HvPagination {...table.getHvPaginationProps?.()} /> )} </HvTableSection> ); };
Filtering
Row filtering and global filtering can be configured by leveraging the useHvFilters
and useHvGlobalFilter
hooks respectively.
Title | Time | Event Type | Status | Probability | Severity | Priority | ||
---|---|---|---|---|---|---|---|---|
Event 1 | 2020-03-20 | Anomaly detection | Closed | 1% | Critical | High | ||
Event 2 | 2020-03-20 | Anomaly detection | Open | 2% | Major | Medium | ||
Event 3 | 2020-03-20 | Anomaly detection | Closed | 3% | Average | Low | ||
Event 4 | 2020-03-20 | Anomaly detection | Open | 4% | Minor | High | ||
Event 5 | 2020-03-20 | Anomaly detection | Closed | 5% | Critical | Medium | ||
Event 6 | 2020-03-20 | Anomaly detection | Open | 6% | Major | Low | ||
Event 7 | 2020-03-20 | Anomaly detection | Closed | 7% | Average | High | ||
Event 8 | 2020-03-20 | Anomaly detection | Open | 8% | Minor | Medium | ||
Event 9 | 2020-03-20 | Anomaly detection | Closed | 9% | Critical | Low | ||
Event 10 | 2020-03-20 | Anomaly detection | Open | 10% | Major | High |
Showrows
10
import { useCallback, useMemo, useState } from "react"; import { HvActionGeneric, HvBulkActions, HvEmptyState, HvIconButton, HvInput, HvPagination, HvRowInstance, HvTab, HvTable, HvTableBody, HvTableCell, HvTableColumnConfig, HvTableContainer, HvTableHead, HvTableHeader, HvTableInstance, HvTableOptions, HvTableRow, HvTableSection, HvTableState, HvTabs, useHvBulkActions, useHvFilters, useHvGlobalFilter, useHvPagination, useHvRowSelection, useHvSortBy, useHvTable, useHvTableSticky, } from "@hitachivantara/uikit-react-core"; import { Ban, Delete, Duplicate, Preview, } from "@hitachivantara/uikit-react-icons"; import { AssetEvent, makeData } from "./makeData"; export default function Demo() { const [data, setData] = useState(() => makeData(128)); const deleteRow = useCallback((row: HvRowInstance<AssetEvent>) => { setData((prev) => prev.filter((r) => r.id !== row.original.id)); }, []); const columns = useMemo<HvTableColumnConfig<AssetEvent>[]>( () => [ { Header: "Title", accessor: "name", style: { minWidth: 120 } }, { Header: "Time", accessor: "createdDate", style: { minWidth: 100 } }, { Header: "Event Type", accessor: "eventType", style: { minWidth: 100 } }, { Header: "Status", accessor: "status", style: { minWidth: 100 } }, { Header: "Probability", accessor: "riskScore", align: "right", Cell: ({ value }) => `${value}%`, }, { Header: "Severity", accessor: "severity" }, { Header: "Priority", accessor: "priority" }, { id: "delete", variant: "actions", Cell: ({ row }) => ( <HvIconButton title="Delete" onClick={() => deleteRow(row)}> <Delete /> </HvIconButton> ), }, ], [deleteRow], ); return ( <MyTable data={data} columns={columns} bulkActions={[ { id: "clone", label: "Clone", icon: <Duplicate /> }, { id: "remove", label: "Remove", icon: <Delete /> }, { id: "preview", label: "Preview", icon: <Preview />, disabled: true }, ]} onBulkAction={(evt, action, selectedRows) => { if (action.id === "remove") { selectedRows.forEach(deleteRow); } else if (action.id === "clone") { const clonedData = selectedRows.map(({ original }) => ({ ...original, id: `${original.id}-copy`, name: `${original.name}-copy`, })); setData((prev) => prev.concat(clonedData)); } }} options={{ // other `useHvTable` options getRowId: (row) => row.id, initialState: { pageSize: 10, }, }} /> ); } /** * A generic client-side table. * Includes row selection & sorting, bulk actions, pagination, sticky headers. */ export const MyTable = <T extends object>(props: { columns: HvTableColumnConfig<T>[]; data: T[] | undefined; initialState?: Partial<HvTableState<T>>; bulkActions?: HvActionGeneric[]; onBulkAction?: ( event: React.SyntheticEvent, action: HvActionGeneric, selectedRows: HvTableInstance<T>["selectedFlatRows"], ) => void; options?: HvTableOptions<T>; }) => { const { columns, data, bulkActions, onBulkAction, options } = props; const table = useHvTable<T>( { columns, data, stickyHeader: true, ...options, }, useHvTableSticky, useHvSortBy, useHvFilters, useHvGlobalFilter, useHvPagination, useHvRowSelection, useHvBulkActions, ); const renderTableRow = (i: number) => { const row = table.page[i]; if (!row) { // render up to 16 <EmptyRow> when there are multiple pages const showEmptyRow = table.pageCount && table.pageCount > 1 && i < 16; if (!showEmptyRow) return null; return ( <HvTableRow key={`empty-${i}`}> <HvTableCell colSpan={100} /> </HvTableRow> ); } table.prepareRow(row); return ( <HvTableRow {...row.getRowProps()} key={row.getRowProps().key}> {row.cells.map((cell) => ( <HvTableCell className="text-nowrap" {...cell.getCellProps()} key={cell.getCellProps().key} > {cell.render("Cell")} </HvTableCell> ))} </HvTableRow> ); }; return ( <HvTableSection raisedHeader title={ <HvTabs value={table.state.filters?.[0]?.value || ""} onChange={(evt, value) => { table.setFilter?.("status", value || undefined); }} > <HvTab value="" label="All" /> <HvTab value="open" label="Open" /> <HvTab value="closed" label="Closed" /> </HvTabs> } actions={ <HvInput type="search" placeholder="Search all columns" onChange={(e, v) => table.setGlobalFilter?.(v)} /> } > {table.page.length > 0 && ( <HvBulkActions actions={bulkActions} maxVisibleActions={1} onAction={(evt, action) => { onBulkAction?.(evt, action, table.selectedFlatRows); }} {...table.getHvBulkActionsProps?.()} /> )} <HvTableContainer className="max-h-500px"> <HvTable {...table.getTableProps()}> <HvTableHead {...table.getTableHeadProps?.()}> {table.headerGroups.map((headerGroup) => ( <HvTableRow {...headerGroup.getHeaderGroupProps()} key={headerGroup.getHeaderGroupProps().key} > {headerGroup.headers.map((col) => ( <HvTableHeader {...col.getHeaderProps()} key={col.getHeaderProps().key} > {col.render("Header")} </HvTableHeader> ))} </HvTableRow> ))} </HvTableHead> <HvTableBody {...table.getTableBodyProps()}> {table.page.length > 0 ? ( [...Array(table.state.pageSize).keys()].map(renderTableRow) ) : ( <HvTableRow> <HvTableCell colSpan={100} style={{ height: 96 }}> <HvEmptyState message="No data to display" icon={<Ban />} /> </HvTableCell> </HvTableRow> )} </HvTableBody> </HvTable> </HvTableContainer> {table.page.length > 0 && ( <HvPagination {...table.getHvPaginationProps?.()} /> )} </HvTableSection> ); };
Grouping
Example of header grouping and row expansion via grouping
Status | Title | Time | Type | Probability | Event | |
---|---|---|---|---|---|---|
Severity | Priority | |||||
Closed (6) | Avg. 6% | |||||
Event 1 | 2020-03-20 | Anomaly detection | 1% | Critical | High | |
Event 3 | 2020-03-20 | Anomaly detection | 3% | Average | Low | |
Event 5 | 2020-03-20 | Anomaly detection | 5% | Critical | Medium | |
Event 7 | 2020-03-20 | Anomaly detection | 7% | Average | High | |
Event 9 | 2020-03-20 | Anomaly detection | 9% | Critical | Low | |
Event 11 | 2020-03-20 | Anomaly detection | 11% | Average | Medium | |
Open (6) | Avg. 7% | |||||
Event 2 | 2020-03-20 | Anomaly detection | 2% | Major | Medium | |
Event 4 | 2020-03-20 | Anomaly detection | 4% | Minor | High | |
Event 6 | 2020-03-20 | Anomaly detection | 6% | Major | Low | |
Event 8 | 2020-03-20 | Anomaly detection | 8% | Minor | Medium | |
Event 10 | 2020-03-20 | Anomaly detection | 10% | Major | High | |
Event 12 | 2020-03-20 | Anomaly detection | 12% | Minor | Low |
import { useState } from "react"; import { useGroupBy } from "react-table"; import { HvCellInstance, HvRowInstance, HvTable, HvTableBody, HvTableCell, HvTableColumnConfig, HvTableContainer, HvTableHead, HvTableHeader, HvTableRow, HvTableSection, HvTableState, theme, useHvHeaderGroups, useHvRowExpand, useHvTable, } from "@hitachivantara/uikit-react-core"; import { makeData, type AssetEvent } from "./makeData"; const columns: HvTableColumnConfig<AssetEvent>[] = [ { Header: "Title", accessor: "name", style: { minWidth: 120 } }, { Header: "Time", accessor: "createdDate", style: { minWidth: 100 } }, { Header: "Type", accessor: "eventType", style: { minWidth: 100 } }, { Header: "Status", accessor: "status", style: { width: 140 } }, { Header: "Probability", accessor: "riskScore", align: "right", // numeric values should be right-aligned Cell: ({ value }) => `${value}%`, aggregate: "average", Aggregated: ({ value }) => `Avg. ${value}%`, }, { Header: "Event", id: "eventGroup", columns: [ { Header: "Severity", accessor: "severity" }, { Header: "Priority", accessor: "priority" }, ], }, ]; export default function Demo() { const [data] = useState(() => makeData(12)); return ( <Table<AssetEvent> data={data} columns={columns} initialState={{ groupBy: ["status"], expanded: { "status:Closed": true, "status:Open": true, }, }} /> ); } export function Table<T extends object>({ data, columns, initialState, }: { data: T[]; columns: HvTableColumnConfig<T>[]; initialState: Partial<HvTableState<T>>; }) { const table = useHvTable<T>( { columns, data, initialState }, useGroupBy, useHvHeaderGroups, useHvRowExpand, ); const renderCellContent = ( cell: HvCellInstance<T>, row: HvRowInstance<T>, ) => { if (cell.isGrouped) { // If it's a grouped cell, add an expander and row count return ( <> {cell.render("Cell")} ({row.subRows.length}) </> ); } // If the cell is aggregated, use the Aggregated renderer if (cell.isAggregated) return cell.render("Aggregated"); // For cells with repeated values, render null if (cell.isPlaceholder) return null; // return the default cell renderer otherwise return cell.render("Cell"); }; return ( <HvTableSection> <HvTableContainer> <HvTable {...table.getTableProps()}> <HvTableHead> {table.headerGroups.map((headerGroup) => ( <HvTableRow {...headerGroup.getHeaderGroupProps()} key={headerGroup.getHeaderGroupProps().key} > {headerGroup.headers.map((col) => ( <HvTableHeader {...col.getHeaderProps()} key={col.getHeaderProps().key} > {col.render("Header")} </HvTableHeader> ))} </HvTableRow> ))} </HvTableHead> <HvTableBody {...table.getTableBodyProps()}> {table.rows.map((row: HvRowInstance<T>) => { table.prepareRow(row); const { key, ...rowProps } = row.getRowProps(); const hasSubRows = row.subRows.length > 0; return ( <HvTableRow key={key} style={{ backgroundColor: theme.colors[hasSubRows ? "atmo1" : "atmo2"], }} {...rowProps} > {row.cells.map((cell) => ( <HvTableCell {...cell.getCellProps()} key={cell.getCellProps().key} > {renderCellContent(cell, row)} </HvTableCell> ))} </HvTableRow> ); })} </HvTableBody> </HvTable> </HvTableContainer> </HvTableSection> ); }
Other examples
You can find more table examples in our Stackblitz Tables collection . In there, you’ll find various custom table use-cases, such as server-side , infinite scroll , sticky column resizing , etc.