InteractiveTable: Add expand all to column (#84966)
* feat: add expand all to InteractiveTable * chore: pr feedback; type tightening and better testing practice * chore: pr feedback; type cleanup
This commit is contained in:
@@ -217,6 +217,8 @@ export const availableIconsIndex = {
|
||||
sync: true,
|
||||
'sync-slash': true,
|
||||
table: true,
|
||||
'table-collapse-all': true,
|
||||
'table-expand-all': true,
|
||||
'tag-alt': true,
|
||||
'telegram-alt': true,
|
||||
'text-fields': true,
|
||||
|
||||
+17
-2
@@ -1,8 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { CellProps } from 'react-table';
|
||||
import { CellProps, HeaderProps } from 'react-table';
|
||||
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
|
||||
const expanderContainerStyles = css({
|
||||
display: 'flex',
|
||||
@@ -27,3 +27,18 @@ export function ExpanderCell<K extends object>({ row, __rowID }: CellProps<K, vo
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpanderHeader<K extends object>({ isAllRowsExpanded, toggleAllRowsExpanded }: HeaderProps<K>) {
|
||||
return (
|
||||
<div className={expanderContainerStyles}>
|
||||
<IconButton
|
||||
aria-label={!isAllRowsExpanded ? 'Expand all rows' : 'Collapse all rows'}
|
||||
name={!isAllRowsExpanded ? 'table-expand-all' : 'table-collapse-all'}
|
||||
onClick={() => toggleAllRowsExpanded()}
|
||||
size={'lg'}
|
||||
tooltip={!isAllRowsExpanded ? 'Expand all rows' : 'Collapse all rows'}
|
||||
variant={'secondary'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -111,6 +111,7 @@ export const MyComponent = () => {
|
||||
data={tableData}
|
||||
getRowId={(r) => r.datasource}
|
||||
renderExpandedRow={ExpandedCell}
|
||||
showExpandAll
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,151 +1,22 @@
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Meta, StoryFn, StoryObj } from '@storybook/react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { InteractiveTable, Column, CellProps, LinkButton } from '@grafana/ui';
|
||||
import { InteractiveTable, CellProps, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { FetchDataArgs, InteractiveTableHeaderTooltip } from './InteractiveTable';
|
||||
import mdx from './InteractiveTable.mdx';
|
||||
|
||||
const EXCLUDED_PROPS = ['className', 'renderExpandedRow', 'getRowId', 'fetchData'];
|
||||
|
||||
const meta: Meta<typeof InteractiveTable> = {
|
||||
title: 'Experimental/InteractiveTable',
|
||||
component: InteractiveTable,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: EXCLUDED_PROPS,
|
||||
},
|
||||
},
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
interface TableData {
|
||||
header1: string;
|
||||
header2?: number;
|
||||
noheader?: string;
|
||||
}
|
||||
|
||||
export const Basic: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
const columns = useMemo<Array<Column<TableData>>>(
|
||||
() => [
|
||||
{ id: 'header2', header: 'With missing values', sortType: 'number', disableGrow: true },
|
||||
{
|
||||
id: 'noheader',
|
||||
sortType: 'number',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
const data: TableData[] = useMemo(
|
||||
() => [
|
||||
{ header1: 'a', header2: 1 },
|
||||
{ header1: 'b', noheader: "This column doesn't have an header" },
|
||||
{ header1: 'c', noheader: "But it's still sortable" },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return <InteractiveTable columns={columns} data={data} getRowId={(r) => r.header1} />;
|
||||
};
|
||||
|
||||
interface WithRowExpansionData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ExpandedCell = ({ description }: WithRowExpansionData) => {
|
||||
return <p>{description}</p>;
|
||||
};
|
||||
|
||||
export const WithRowExpansion: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
const tableData: WithRowExpansionData[] = [
|
||||
{
|
||||
datasource: 'Prometheus',
|
||||
repo: 'https://github.com/prometheus/prometheus',
|
||||
description: 'Open source time series database & alerting.',
|
||||
},
|
||||
{
|
||||
datasource: 'Loki',
|
||||
repo: 'https://github.com/grafana/loki',
|
||||
description: 'Like Prometheus but for logs. OSS logging solution from Grafana Labs.',
|
||||
},
|
||||
{
|
||||
datasource: 'Tempo',
|
||||
repo: 'https://github.com/grafana/tempo',
|
||||
description: 'High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.',
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<Column<WithRowExpansionData>> = [
|
||||
{ id: 'datasource', header: 'Data Source' },
|
||||
{ id: 'repo', header: 'Repo' },
|
||||
];
|
||||
|
||||
return (
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
getRowId={(r) => r.datasource}
|
||||
renderExpandedRow={ExpandedCell}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface WithCustomCellData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const RepoCell = ({
|
||||
row: {
|
||||
original: { repo },
|
||||
},
|
||||
}: CellProps<WithCustomCellData, void>) => {
|
||||
return (
|
||||
<LinkButton href={repo} size="sm" icon="external-link-alt">
|
||||
Open on GithHub
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCustomCell: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
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 },
|
||||
];
|
||||
|
||||
return <InteractiveTable columns={columns} data={tableData} getRowId={(r) => r.datasource} />;
|
||||
};
|
||||
|
||||
interface WithPaginationData {
|
||||
interface CarData {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
car: string;
|
||||
car?: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const pageableData: WithPaginationData[] = [
|
||||
const pageableData: CarData[] = [
|
||||
{ id: '48a3926a-e82c-4c26-b959-3a5f473e186e', firstName: 'Brynne', lastName: 'Denisevich', car: 'Cougar', age: 47 },
|
||||
{
|
||||
id: 'cf281390-adbf-4407-8cf3-a52e012f63e6',
|
||||
@@ -228,60 +99,155 @@ const pageableData: WithPaginationData[] = [
|
||||
{ id: 'b9b0b559-acc1-4bd8-b052-160ecf3e4f68', firstName: 'Ermanno', lastName: 'Sinott', car: 'Thunderbird', age: 26 },
|
||||
];
|
||||
|
||||
export const WithPagination: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
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} />;
|
||||
const meta: Meta<typeof InteractiveTable<CarData>> = {
|
||||
title: 'Experimental/InteractiveTable',
|
||||
component: InteractiveTable,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: EXCLUDED_PROPS,
|
||||
},
|
||||
},
|
||||
args: {
|
||||
columns: [
|
||||
{ id: 'firstName', header: 'First name', sortType: 'string' },
|
||||
{ id: 'lastName', header: 'Last name', sortType: 'string' },
|
||||
{ id: 'car', header: 'Car', sortType: 'string' },
|
||||
{ id: 'age', header: 'Age' },
|
||||
],
|
||||
data: pageableData.slice(0, 10),
|
||||
getRowId: (r) => r.id,
|
||||
pageSize: 0,
|
||||
showExpandAll: false,
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export const WithHeaderTooltips: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
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' },
|
||||
];
|
||||
type TableStory = StoryObj<typeof InteractiveTable<CarData>>;
|
||||
|
||||
const headerTooltips: Record<string, InteractiveTableHeaderTooltip> = {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
export const Basic: TableStory = {
|
||||
args: {
|
||||
columns: [
|
||||
{
|
||||
id: 'firstName',
|
||||
header: 'First name',
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
iconName: 'plus-square',
|
||||
},
|
||||
};
|
||||
{
|
||||
id: 'lastName',
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
{ id: 'car', header: 'With missing values', sortType: 'alphanumeric', disableGrow: true },
|
||||
],
|
||||
data: [
|
||||
{
|
||||
id: 'be5736f5-7015-4668-a03d-44b56f2b012c',
|
||||
firstName: 'Sonni',
|
||||
lastName: 'Still sortable!',
|
||||
car: 'Legend',
|
||||
age: 75,
|
||||
},
|
||||
{ id: 'fdbe3559-c68a-4f2f-b579-48ef02642628', firstName: 'Hanson', lastName: 'Giraudeau', car: 'X5', age: 67 },
|
||||
{ id: '7d0ee01a-7ac5-4e0a-9c73-e864d10c0152', firstName: 'Whitman', lastName: 'Seabridge', age: 99 },
|
||||
{ id: '177c2287-b7cb-4b5f-8976-56ee993bed61', firstName: 'Aleda', lastName: 'Friman', car: 'X5', age: 44 },
|
||||
{ id: '87c21e60-c2f4-4a01-b2af-a6d22c196e25', firstName: 'Cullen', lastName: 'Kobpac', car: 'Montero', age: 28 },
|
||||
{ id: 'dd89f32d-2ef4-4c35-8e23-a8b2219e3a69', firstName: 'Fitz', lastName: 'Butterwick', car: 'Fox', age: 70 },
|
||||
{
|
||||
id: 'cc1b4de7-8ec5-49bd-93bc-bee9fa1ccf37',
|
||||
firstName: 'Jordon',
|
||||
lastName: 'Harrington',
|
||||
car: 'Elantra',
|
||||
age: 39,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ExpandedCell = ({ car }: CarData) => {
|
||||
return <p>{car}</p>;
|
||||
};
|
||||
|
||||
export const WithRowExpansion: TableStory = {
|
||||
args: {
|
||||
renderExpandedRow: ExpandedCell,
|
||||
},
|
||||
};
|
||||
|
||||
interface WithCustomCellData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const RepoCell = ({
|
||||
row: {
|
||||
original: { repo },
|
||||
},
|
||||
}: CellProps<WithCustomCellData, void>) => {
|
||||
return (
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={pageableData.slice(0, 10)}
|
||||
getRowId={(r) => r.id}
|
||||
headerTooltips={headerTooltips}
|
||||
/>
|
||||
<LinkButton href={repo} size="sm" icon="external-link-alt">
|
||||
Open on GithHub
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCustomCell: StoryObj<typeof InteractiveTable<WithCustomCellData>> = {
|
||||
args: {
|
||||
columns: [
|
||||
{ id: 'datasource', header: 'Data Source' },
|
||||
{ id: 'repo', header: 'Repo', cell: RepoCell },
|
||||
],
|
||||
data: [
|
||||
{
|
||||
datasource: 'Prometheus',
|
||||
repo: 'https://github.com/prometheus/prometheus',
|
||||
},
|
||||
{
|
||||
datasource: 'Loki',
|
||||
repo: 'https://github.com/grafana/loki',
|
||||
},
|
||||
{
|
||||
datasource: 'Tempo',
|
||||
repo: 'https://github.com/grafana/tempo',
|
||||
},
|
||||
],
|
||||
getRowId: (r) => r.datasource,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPagination: TableStory = {
|
||||
args: {
|
||||
pageSize: 15,
|
||||
data: pageableData,
|
||||
},
|
||||
};
|
||||
|
||||
const headerTooltips: Record<string, InteractiveTableHeaderTooltip> = {
|
||||
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',
|
||||
},
|
||||
};
|
||||
export const WithHeaderTooltips: TableStory = {
|
||||
args: {
|
||||
headerTooltips,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const fetchData = useCallback(({ sortBy }: FetchDataArgs<WithPaginationData>) => {
|
||||
const fetchData = useCallback(({ sortBy }: FetchDataArgs<CarData>) => {
|
||||
if (!sortBy?.length) {
|
||||
return setData(pageableData);
|
||||
}
|
||||
@@ -291,9 +257,9 @@ export const WithControlledSort: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
newData.sort((a, b) => {
|
||||
const sort = sortBy[0];
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const aData = a[sort.id as keyof Omit<WithPaginationData, 'age'>];
|
||||
const aData = a[sort.id as keyof Omit<CarData, 'age' | 'car'>];
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const bData = b[sort.id as keyof Omit<WithPaginationData, 'age'>];
|
||||
const bData = b[sort.id as keyof Omit<CarData, 'age' | 'car'>];
|
||||
if (sort.desc) {
|
||||
return bData.localeCompare(aData);
|
||||
}
|
||||
@@ -303,6 +269,7 @@ export const WithControlledSort: StoryFn<typeof InteractiveTable> = (args) => {
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
return <InteractiveTable columns={columns} data={data} getRowId={(r) => r.id} pageSize={15} fetchData={fetchData} />;
|
||||
return <InteractiveTable {...args} data={data} pageSize={15} fetchData={fetchData} />;
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -77,28 +77,101 @@ describe('InteractiveTable', () => {
|
||||
expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
});
|
||||
describe('row expansion', () => {
|
||||
it('correctly expands rows', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
const { user } = setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
it('correctly expands rows', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
const { user } = setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
/>
|
||||
);
|
||||
const expanderButton = screen.getByRole('button', { name: /toggle row expanded/i });
|
||||
await user.click(expanderButton);
|
||||
|
||||
const expanderButton = screen.getByRole('button', { name: /toggle row expanded/i });
|
||||
await user.click(expanderButton);
|
||||
expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden');
|
||||
|
||||
expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden');
|
||||
expect(expanderButton.getAttribute('aria-controls')).toBe(
|
||||
// ancestor tr's id should match the expander button's aria-controls attribute
|
||||
screen.getByTestId('test-1').parentElement?.parentElement?.id
|
||||
);
|
||||
});
|
||||
it('does not render expand all when showExpandAll is false', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
showExpandAll={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(expanderButton.getAttribute('aria-controls')).toBe(
|
||||
// ancestor tr's id should match the expander button's aria-controls attribute
|
||||
screen.getByTestId('test-1').parentElement?.parentElement?.id
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: 'Expand all rows' })).not.toBeInTheDocument();
|
||||
});
|
||||
it('does not render expand all when showExpandAll is not provided', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Expand all rows' })).not.toBeInTheDocument();
|
||||
});
|
||||
it('renders expand all when showExpandAll is true', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
showExpandAll
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Expand all rows' })).toBeInTheDocument();
|
||||
});
|
||||
it('expands all rows when expand all is clicked', async () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [
|
||||
{ id: '1', value: '1', country: 'Sweden' },
|
||||
{ id: '2', value: '2', country: 'Belgium' },
|
||||
{ id: '3', value: '3', country: 'France' },
|
||||
];
|
||||
const { user } = setup(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
showExpandAll
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('test-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('test-2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('test-3')).not.toBeInTheDocument();
|
||||
|
||||
const expandAllButton = screen.getByRole('button', { name: 'Expand all rows' });
|
||||
await user.click(expandAllButton);
|
||||
|
||||
expect(screen.queryByTestId('test-1')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('test-2')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('test-3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('pagination', () => {
|
||||
it('does not render pagination controls if pageSize is not set', () => {
|
||||
|
||||
@@ -120,7 +120,8 @@ export type InteractiveTableHeaderTooltip = {
|
||||
|
||||
export type FetchDataArgs<Data> = { sortBy: Array<SortingRule<Data>> };
|
||||
export type FetchDataFunc<Data> = ({ sortBy }: FetchDataArgs<Data>) => void;
|
||||
interface Props<TableData extends object> {
|
||||
|
||||
interface BaseProps<TableData extends object> {
|
||||
className?: string;
|
||||
/**
|
||||
* Table's columns definition. Must be memoized.
|
||||
@@ -142,10 +143,6 @@ interface Props<TableData extends object> {
|
||||
* Number of rows per page. A value of zero disables pagination. Defaults to 0.
|
||||
*/
|
||||
pageSize?: number;
|
||||
/**
|
||||
* Render function for the expanded row. if not provided, the tables rows will not be expandable.
|
||||
*/
|
||||
renderExpandedRow?: (row: TableData) => ReactNode;
|
||||
/**
|
||||
* A custom function to fetch data when the table is sorted. If not provided, the table will be sorted client-side.
|
||||
* It's important for this function to have a stable identity, e.g. being wrapped into useCallback to prevent unnecessary
|
||||
@@ -154,6 +151,24 @@ interface Props<TableData extends object> {
|
||||
fetchData?: FetchDataFunc<TableData>;
|
||||
}
|
||||
|
||||
interface WithExpandableRow<TableData extends object> extends BaseProps<TableData> {
|
||||
/**
|
||||
* Render function for the expanded row. if not provided, the tables rows will not be expandable.
|
||||
*/
|
||||
renderExpandedRow: (row: TableData) => ReactNode;
|
||||
/**
|
||||
* Whether to show the "Expand all" button. Depends on renderExpandedRow to be provided. Defaults to false.
|
||||
*/
|
||||
showExpandAll?: boolean;
|
||||
}
|
||||
|
||||
interface WithoutExpandableRow<TableData extends object> extends BaseProps<TableData> {
|
||||
renderExpandedRow?: never;
|
||||
showExpandAll?: never;
|
||||
}
|
||||
|
||||
type Props<TableData extends object> = WithExpandableRow<TableData> | WithoutExpandableRow<TableData>;
|
||||
|
||||
/** @alpha */
|
||||
export function InteractiveTable<TableData extends object>({
|
||||
className,
|
||||
@@ -163,12 +178,13 @@ export function InteractiveTable<TableData extends object>({
|
||||
headerTooltips,
|
||||
pageSize = 0,
|
||||
renderExpandedRow,
|
||||
showExpandAll = false,
|
||||
fetchData,
|
||||
}: Props<TableData>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const tableColumns = useMemo(() => {
|
||||
return getColumns<TableData>(columns);
|
||||
}, [columns]);
|
||||
return getColumns<TableData>(columns, showExpandAll);
|
||||
}, [columns, showExpandAll]);
|
||||
const id = useUniqueId();
|
||||
const getRowHTMLID = useCallback(
|
||||
(row: Row<TableData>) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Column as RTColumn } from 'react-table';
|
||||
|
||||
import { ExpanderCell } from './ExpanderCell';
|
||||
import { ExpanderCell, ExpanderHeader } from './Expander';
|
||||
import { Column } from './types';
|
||||
|
||||
export const EXPANDER_CELL_ID = '__expander' as const;
|
||||
@@ -10,11 +10,17 @@ type InternalColumn<T extends object> = RTColumn<T> & {
|
||||
};
|
||||
|
||||
// Returns the columns in a "react-table" acceptable format
|
||||
export function getColumns<K extends object>(columns: Array<Column<K>>): Array<InternalColumn<K>> {
|
||||
export function getColumns<K extends object>(
|
||||
columns: Array<Column<K>>,
|
||||
showExpandAll = false
|
||||
): Array<InternalColumn<K>> {
|
||||
return [
|
||||
{
|
||||
id: EXPANDER_CELL_ID,
|
||||
Cell: ExpanderCell,
|
||||
...(showExpandAll && {
|
||||
Header: ExpanderHeader,
|
||||
}),
|
||||
disableSortBy: true,
|
||||
width: 0,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.35355 7.64645C8.15829 7.45118 7.84171 7.45118 7.64645 7.64645L4.46447 10.8284C4.2692 11.0237 4.2692 11.3403 4.46447 11.5355C4.65973 11.7308 4.97631 11.7308 5.17157 11.5355L8 8.70711L10.8284 11.5355C11.0237 11.7308 11.3403 11.7308 11.5355 11.5355C11.7308 11.3403 11.7308 11.0237 11.5355 10.8284L8.35355 7.64645ZM8.5 14L8.5 8L7.5 8L7.5 14L8.5 14Z"
|
||||
fill="currentColor"/>
|
||||
<line x1="2" y1="5.5" x2="14" y2="5.5" stroke="currentColor"/>
|
||||
<line x1="2" y1="2.5" x2="14" y2="2.5" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="currentColor" fill="transparent" d="M2.5 5.5h11v2H2.5Z"/>
|
||||
<path stroke="currentColor" fill="transparent" d="M2.5 1.5h11v2H2.5Z"/>
|
||||
<path
|
||||
d="M7.64645 14.3536C7.84171 14.5488 8.15829 14.5488 8.35355 14.3536L11.5355 11.1716C11.7308 10.9763 11.7308 10.6597 11.5355 10.4645C11.3403 10.2692 11.0237 10.2692 10.8284 10.4645L8 13.2929L5.17157 10.4645C4.97631 10.2692 4.65973 10.2692 4.46447 10.4645C4.2692 10.6597 4.2692 10.9763 4.46447 11.1716L7.64645 14.3536ZM7.5 8V14H8.5V8H7.5Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 592 B |
Reference in New Issue
Block a user