packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx
import { Meta, ArgTypes, Story } from '@storybook/addon-docs/blocks';
import { InteractiveTable } from './InteractiveTable'; import { Badge } from '../Badge/Badge';
<Meta title="MDX|InteractiveTable" component={InteractiveTable} />The InteractiveTable is used to display and select data efficiently. It allows for the display and modification of detailed information. With additional functionality it allows for batch editing, as needed by your feature's users.
It is a wrapper around React Table, for more information, refer to the official documentation.
The InteractiveTable can be used to allow users to perform administrative tasks workflows.
The InteractiveTable component supports workflows where the user needs to manipulate potentially large datasets. In its simplest form it allows for batch selection of existing data for further processing within a larger workflow. Its capabilities can be expanded to enable dataset creation as well as batch editing.
The table will typically be complemented by a section above to support either read-only or editing workflows. Read-only tables will usually have a summary of specific properties or calculated attributes of the dataset. Depending on the feature under design there may be import/export options. For editing workflows users will add new rows to the table below. There will also be batch edit options for existing rows in the header section.
Individual rows can be expanded to display additional details or reconfigure properties previously defined when the row was created. In the case of editing a row, the UI presented should be a duplicate of the header section above the table used to create the row. In the case where a property can no longer be edited it should still be displayed but disabled/greyed out. The expanded row area should be used to declutter the primary presentation of data. Carefully consider what the user needs to know at first glance and what can be hidden behind the Row Expander button. In general, data-types that are consistent across all dataset are in the primary table, variances are pushed to the expanded section for each individual row.
It is important to understand and define these as requirements for the feature under design as the InteractiveTable is not responsive and will not provide good UX on small or touch screens.
| Element | Details |
|---|---|
| Tri-state batch selector | The tri-state checkbox can be checked, not checked, or partially checked. The condition of being partially checked is based on the selection of child elements. If all child elements are selected, the parent checkbox is checked. If some child elements are selected, the parent checkbox is partially checked. |
| Row selector | Select, deselect row |
| Row expand/collapse | The expander control is used to show and hide additional details and potentially controls/modifiers, and therefore declutter your app |
| Row details | This section is utilised to either show additional details or to provide the user with row specific controls or modifiers. Typically batch editing controls found outside the table should not be duplicated here. |
| Column Label | Descriptive identifier or header assigned to a column |
| Column Sort | Sort alphabetically, numerically etc. |
| Row level Read-Only actions | Duplicate |
| Row level Editing action | Duplicate, Delete |
Where a table has a mix of read-only and editable elements, when the user selects read-only elements the editing batch operations above the table become disabled. Consider adding a column to indicate which rows are editable and which are read-only.
columns and data PropsTo avoid unnecessary rerenders, columns and data must be memoized.
Columns are rendered in the same order defined in the columns prop.
Each Cell's content is automatically rendered by matching the id of the column to the key of each object in the data array prop.
interface TableData {
projectName: string;
repository: string;
}
const columns = useMemo<Array<Column<TableData>>>(
() => [
id: 'projectName'
header: "Project Name"
],
[
id: 'repository',
header: "Repository"
],
[]
);
const data = useMemo<Array<TableData>>(
() => [
{
projectName: 'Grafana',
repository: 'https://github.com/grafana/grafana',
}
],
[
{
projectName: 'Loki';
repository: 'https://github.com/grafana/loki';
}
],
[]
);
Individual rows can be expanded to display additional details or reconfigure properties previously defined when the row was created. The expanded row area should be used to unclutter the primary presentation of data, carefully consider what the user needs to know at first glance and what can be hidden behind the Row Expander button.
In general, data-types that are consistent across all dataset are in the primary table, variances are pushed to the expanded section for each individual row.
<Story id="experimental-interactivetable--with-row-expansion" />Row expansion is enabled whenever the renderExpanded prop is provided. The renderExpanded function is called with the row's data and should return a ReactNode.
interface TableData {
datasource: string;
repo: string;
description: string;
}
const tableData: TableData[] = [
//...
];
const columns: Array<Column<TableData>> = [
//...
];
const ExpandedCell = ({ description }: TableData) => {
return <p>{description}</p>;
};
export const MyComponent = () => {
return (
<InteractiveTable
columns={columns}
data={tableData}
getRowId={(r) => r.datasource}
renderExpandedRow={ExpandedCell}
showExpandAll
/>
);
};
Column headers can be customized using strings, React elements, or renderer functions. The header property accepts any value that matches React Table's Renderer type.
Important: When using custom header content, prefer inline elements (like <span>) over block elements (like <div>) to avoid layout issues. Block-level elements can cause extra spacing and alignment problems in table headers because they disrupt the table's inline flow. Use display: inline-flex or display: inline-block when you need flexbox or block-like behavior.
const columns: Array<Column<TableData>> = [
// React element header
{
id: 'checkbox',
header: (
<>
<label htmlFor="select-all" className="sr-only">
Select all rows
</label>
<Checkbox id="select-all" />
</>
),
cell: () => <Checkbox aria-label="Select row" />,
},
// Function renderer header
{
id: 'firstName',
header: () => (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}>
<Icon name="user" size="sm" />
<span>First Name</span>
</span>
),
},
// String header
{ id: 'lastName', header: 'Last name' },
];
Individual cells can be rendered using custom content dy defining a cell property on the column definition.
interface TableData {
datasource: string;
repo: string;
}
const RepoCell = ({
row: {
original: { repo },
},
}: CellProps<WithCustomCellData, void>) => {
return (
<LinkButton href={repo} size="sm" icon="external-link-alt">
Open on GitHub
</LinkButton>
);
};
const tableData: WithCustomCellData[] = [
{
datasource: 'Prometheus',
repo: 'https://github.com/prometheus/prometheus',
},
{
datasource: 'Loki',
repo: 'https://github.com/grafana/loki',
},
{
datasource: 'Tempo',
repo: 'https://github.com/grafana/tempo',
},
];
const columns: Array<Column<WithCustomCellData>> = [
{ id: 'datasource', header: 'Data Source' },
{ id: 'repo', header: 'Repo', cell: RepoCell },
];
export const MyComponent = () => {
return <InteractiveTable columns={columns} data={tableData} getRowId={(r) => r.datasource} />;
};
The table can be rendered with pagination controls by passing in the pageSize property. All data must be provided as
only client side pagination is supported.
interface WithPaginationData {
id: string;
firstName: string;
lastName: string;
car: string;
age: number;
}
export const MyComponent = () => {
const pageableData: WithPaginationData[] = [
{ id: '48a3926a-e82c-4c26-b959-3a5f473e186e', firstName: 'Brynne', lastName: 'Denisevich', car: 'Cougar', age: 47 },
{
id: 'cf281390-adbf-4407-8cf3-a52e012f63e6',
firstName: 'Aldridge',
lastName: 'Shirer',
car: 'Viper RT/10',
age: 74,
},
// ...
{
id: 'b9b0b559-acc1-4bd8-b052-160ecf3e4f68',
firstName: 'Ermanno',
lastName: 'Sinott',
car: 'Thunderbird',
age: 26,
},
];
const columns: Array<Column<WithPaginationData>> = [
{ id: 'firstName', header: 'First name' },
{ id: 'lastName', header: 'Last name' },
{ id: 'car', header: 'Car', sortType: 'string' },
{ id: 'age', header: 'Age', sortType: 'number' },
];
return <InteractiveTable columns={columns} data={pageableData} getRowId={(r) => r.id} pageSize={15} />;
};
It may be useful to render a tooltip on the header of a column to provide additional information about the data in that column.
<Story id="experimental-interactivetable--with-header-tooltips" />interface WithPaginationData {
id: string;
firstName: string;
lastName: string;
car: string;
age: number;
}
export const MyComponent = () => {
const pageableData: WithPaginationData[] = [
{ id: '48a3926a-e82c-4c26-b959-3a5f473e186e', firstName: 'Brynne', lastName: 'Denisevich', car: 'Cougar', age: 47 },
{
id: 'cf281390-adbf-4407-8cf3-a52e012f63e6',
firstName: 'Aldridge',
lastName: 'Shirer',
car: 'Viper RT/10',
age: 74,
},
// ...
{
id: 'b9b0b559-acc1-4bd8-b052-160ecf3e4f68',
firstName: 'Ermanno',
lastName: 'Sinott',
car: 'Thunderbird',
age: 26,
},
];
const columns: Array<Column<WithPaginationData>> = [
{ id: 'firstName', header: 'First name' },
{ id: 'lastName', header: 'Last name' },
{ id: 'car', header: 'Car', sortType: 'string' },
{ id: 'age', header: 'Age', sortType: 'number' },
];
const headerToolTips = {
age: { content: 'The number of years since the person was born' },
lastName: {
content: () => {
return (
<>
<h4>Here is an h4</h4>
<div>Some content</div>
<div>Some more content</div>
</>
);
},
iconName: 'plus-square',
},
};
return (
<InteractiveTable columns={columns} data={pageableData} getRowId={(r) => r.id} headerToolTips={headerToolTips} />
);
};
The default sorting can be changed to controlled sorting by passing in the fetchData function, which is called whenever the sorting changes and should return the sorted data. This is useful when the sorting is done server side. It is important to memoize the fetchData function to prevent unnecessary rerenders and the possibility of an infinite render loop.
interface WithPaginationData {
id: string;
firstName: string;
lastName: string;
car: string;
age: number;
}
export const WithControlledSort: StoryFn<typeof InteractiveTable> = (args) => {
const columns: Array<Column<WithPaginationData>> = [
{ id: 'firstName', header: 'First name', sortType: 'string' },
{ id: 'lastName', header: 'Last name', sortType: 'string' },
{ id: 'car', header: 'Car', sortType: 'string' },
{ id: 'age', header: 'Age' },
];
const [data, setData] = useState(pageableData);
// In production the function will most likely make an API call to fetch the sorted data
const fetchData = useCallback(({ sortBy }: FetchDataArgs<WithPaginationData>) => {
if (!sortBy?.length) {
return setData(pageableData);
}
setTimeout(() => {
const newData = [...pageableData];
newData.sort((a, b) => {
const sort = sortBy[0];
const aData = a[sort.id as keyof Omit<WithPaginationData, 'age'>];
const bData = b[sort.id as keyof Omit<WithPaginationData, 'age'>];
if (sort.desc) {
return bData.localeCompare(aData);
}
return aData.localeCompare(bData);
});
setData(newData);
}, 300);
}, []);
return <InteractiveTable columns={columns} data={data} getRowId={(r) => r.id} pageSize={15} fetchData={fetchData} />;
};