Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement/storybook table #727

Open
wants to merge 11 commits into
base: development/1.0
Choose a base branch
from
Next Next commit
init doc Table
JeanMarcMilletScality committed Jun 4, 2024
commit 8c196665a3121ed3d97152139daec283c940a2a7
87 changes: 87 additions & 0 deletions stories/Table/tablev2.guideline.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
Meta,
Story,
Canvas,
Primary,
Controls,
Unstyled,
Source,
} from '@storybook/blocks';

import * as TableStories from './tablev2.stories.tsx';

<Meta name="Guideline" of={TableStories} />

# Component name

General description of Table

## Usage

General guideline about the component
Do & Don'ts ?

Give it props with columns, data and children.
To display data, Table need to have either SingleSelectableContent or either MultiSelectableContent children.

### Non Selectable Table

Using the SingleSelectTable without passing the ... props will render a simple table.
The Tazble will only render the data with

<Canvas of={TableStories.SimpleTable} layout="fullscreen" />

### Single Selectable Content

Use this table when you want to have the possibility to select only one element of the table.

<Canvas of={TableStories.SingleSelectTable} layout="fullscreen" />

### Multi Selectable Content

Possibility to select one or more element of the table, with the possibility to select all elements with the checkbox on Header.

<Canvas of={TableStories.MultiSelectTable} layout="fullscreen" />

### Search subcomponent

It is possible to pass another subcomponent to Table : Search or SearchWithQueryParams to enable the build-in search input.
Table.Search can be fed with a custom filter function.

<Canvas of={TableStories.TableWithSearch} layout="fullscreen" />

<Canvas of={TableStories.TableWithSearchQueryParams} layout="fullscreen" />

## Variations

### Style Variations

The Table component has no background and will take the color of its parent element.
It is possible to change the color thanks to the SeparationLineColor props

Story with multiple table and different SeparationLineColor

### Status Variations

The Table can take a status prop indicating the status of the data when fetched or queried.
The status accept 4 values 'loading' or 'idle', 'error', and 'success'.
'success' status allow the display of the data fed to the data.
If the status is not defined then the Table behaves as if the status is 'success'.
If there is no data the Table will display an empty state :

<Canvas layout="fullscreen" of={TableStories.TableStatus} />

### Entity name

You can pass an entityName props to table containing singular and plural for each define locale (english and french).
This entityName will be use by the search input and for the status messages.

<Canvas layout="fullscreen" of={TableStories.TableWithEntityName} />
<Controls of={TableStories.TableWithEntityName} include={['locale']} />

## Playground

This is a playground allowing to build and test the component.

<Canvas of={TableStories.Playground} layout="fullscreen" />
<Controls of={TableStories.Playground} />
607 changes: 607 additions & 0 deletions stories/Table/tablev2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,607 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import {
Column,
Table,
TableProps,
} from '../../src/lib/components/tablev2/Tablev2.component';
import { Title } from '../common';
import { BrowserRouter as Router } from 'react-router-dom';
import { CellProps, Row } from 'react-table';
import { Box, Button } from '../../src/lib/next';

import { Stack, Wrap } from '../../src/lib';
import {
SingleSelectableContentProps,
SingleSelectableContent,
} from '../../src/lib/components/tablev2/SingleSelectableContent';
import { useArgs } from '@storybook/preview-api';
import styled from 'styled-components';
import { SearchWithQueryParams } from '../../src/lib/components/tablev2/SearchWithQueryParams';
import { MultiSelectableContent } from '../../src/lib/components/tablev2/MultiSelectableContent';
import { TableSearch } from '../../src/lib/components/tablev2/Search';

const StyledBox = styled(Box)`
height: 150px;
`;

/* ---------------------------------- DATA ---------------------------------- */

const data: Entry[] = [
{
id: 1,
firstName: 'Sotiria-long-long-long-long-long',
lastName: 'Agathangelou',
age: undefined,
health: 'healthy',
},
{
id: 2,
firstName: 'Stefania',
lastName: 'Evgenios',
age: 27,
health: 'warning',
},
{
id: 3,
firstName: 'Yohann',
lastName: 'Rodolph',
age: 27,
health: 'critical',
},
{
id: 4,
firstName: 'Ninette',
lastName: 'Caroline',
age: 31,
health: 'healthy',
},
];

type Entry = {
id: number;
firstName: string;
lastName: string;
age?: number;
health: string;
};

const columns: Column<Entry>[] = [
{
Header: 'First Name',
accessor: 'firstName',
cellStyle: {
textAlign: 'left',
flex: 1,
},
},
{
Header: 'Last Name',
accessor: 'lastName',
cellStyle: {
flex: 1,

textAlign: 'left',
},
// disable the sorting on this column
disableSortBy: true,
},
{
Header: 'Age',
accessor: 'age',
cellStyle: {
flex: 0.5,
textAlign: 'left',
},
},
{
Header: 'Health',
accessor: 'health',
sortType: 'health',
cellStyle: {
flex: 0.5,
textAlign: 'left',
},
},
];
const getRowId = (row: Entry, relativeIndex: number) => {
return row.lastName + ' ' + row.firstName;
};

/* ---------------------------------- STORY --------------------------------- */

type SingleTable = TableProps & SingleSelectableContentProps;
type Story = StoryObj<SingleTable>;

const meta: Meta<SingleTable> = {
title: 'Components/Data Display/Table',
decorators: [
(Story) => (
<Router>
<Box height={'300px'}>
<Story />
</Box>
</Router>
),
],
component: Table,
render: ({ columns, data, ...rest }) => {
// const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
const [{ selectedId }, updateArgs] = useArgs();

return (
<Table
columns={columns}
data={data}
defaultSortingKey={'health'}
{...rest}
>
<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant={rest.separationLineVariant}
selectedId={selectedId}
onRowSelected={(row) => {
action('Table Row Clicked')(row);
updateArgs({ selectedId: row.id });
}}
/>
</Table>
);
},
args: {
// @ts-ignore
columns: columns,
data: data,
defaultSortingKey: 'health',
},
argTypes: {
separationLineVariant: {
control: {
type: 'select',
description: 'Background color',
defaultValue: 'backgroundLevel3',
},
options: [
'backgroundLevel1',
'backgroundLevel2',
'backgroundLevel3',
'backgroundLevel4',
],
},
},
};

export default meta;

export const Playground = {};

/* ---------------------------- SingleSelectTable --------------------------- */

export const SimpleTable: Story = {
render: () => {
return (
<Table columns={columns} data={data} defaultSortingKey={'health'}>
<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
);
},
};

export const SingleSelectTable: Story = {
args: {
// @ts-ignore
getRowId: getRowId,
},
};

export const TableStatus = {
render: (args) => {
return (
<Wrap justifyContent="center" gap="2rem" flexDirection={'column'}>
<StyledBox>
<Table
columns={columns}
data={data}
defaultSortingKey={'health'}
status="loading"
>
<Table.SingleSelectableContent
locale={args.locale}
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
</StyledBox>
<StyledBox>
<Table
columns={columns}
data={data}
defaultSortingKey={'health'}
status="error"
>
<Table.SingleSelectableContent
locale={args.locale}
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
</StyledBox>
<StyledBox>
<Table columns={columns} data={[]} defaultSortingKey={'health'}>
<Table.SingleSelectableContent
locale={args.locale}
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
</StyledBox>
</Wrap>
);
},
};

export const TableWithSearch: Story = {
render: () => {
const [value, setValue] = useState('');
return (
<Table columns={columns} data={data} defaultSortingKey={'health'}>
<Table.Search
value={value}
onChange={(value) => {
setValue(value);
data.filter((data) => JSON.stringify(data).includes(value));
}}
/>

<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
);
},
};

export const TableWithSearchQueryParams: Story = {
render: () => {
return (
<Table columns={columns} data={data} defaultSortingKey={'health'}>
<Table.SearchWithQueryParams
onChange={(value) => {
data.filter((data) => JSON.stringify(data).includes(value));
}}
/>

<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
);
},
};

export const TableWithEntityName: Story = {
render: (args) => {
return (
<Table
columns={columns}
data={data}
defaultSortingKey={'health'}
entityName={{
en: { singular: 'user', plural: 'users' },
fr: { singular: 'utilisateur', plural: 'utilisateurs' },
}}
>
<Table.SearchWithQueryParams locale={args.locale} />
<Table.SingleSelectableContent
locale={args.locale}
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
);
},
argTypes: {
locale: {
options: ['en', 'fr'],
control: 'radio',
},
},
};

export const AsyncTable: Story = {
render: () => {
function DataComponent({
data,
loading,
row,
}: {
row: Row<Entry>;
loading: boolean;
data: string;
}) {
return loading ? (
<span>loading ...</span>
) : (
<span> {`${row.values.health} ${data}`} </span>
);
}

function RowAsync({ row }: { row: Row<Entry> }) {
const [loading, setLoading] = React.useState(true);
const [data, setData] = React.useState('');
React.useEffect(() => {
const timer = setTimeout(() => {
setData('loaded async');
setLoading(false);
}, 1000);
return () => {
clearTimeout(timer);
};
}, []);
return <DataComponent row={row} loading={loading} data={data} />;
}

const renderRowSubComponent = React.useCallback(
({ row, ...rest }: CellProps<Entry>) => {
return <RowAsync row={row} />;
},
[],
);
const columnAsync: Column<Entry>[] = [
{
Header: 'First Name',
accessor: 'firstName',
cellStyle: {
textAlign: 'left',
},
},
{
Header: 'Last Name',
accessor: 'lastName',
cellStyle: {
textAlign: 'left',
},
},
{
Header: 'Age',
accessor: 'age',
cellStyle: {
textAlign: 'left',
},
},
{
Header: 'Health',
accessor: 'health',
sortType: 'health',
cellStyle: {
textAlign: 'left',
},
Cell: renderRowSubComponent,
},
];

return (
<Table columns={columnAsync} data={data} defaultSortingKey={'health'}>
<Table.SearchWithQueryParams></Table.SearchWithQueryParams>
<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
selectedId={'Rodolph Yohann'}
onRowSelected={action('Table Row Clicked')}
/>
</Table>
);
},
};

export const OnBottomCallback: Story = {
render: () => {
const columns: Column<{ index: number; value: number }>[] = [
{
Header: 'value',
accessor: 'value',
cellStyle: {
textAlign: 'left',
},
},
];

const createData = (indexStart = 0) => {
const data: { index: number; value: number }[] = [];

for (let i = 0; i < 100; i++) {
data.push({
index: indexStart + i,
value: Math.floor(Math.random() * 1000),
});
}

return data;
};

const [randomData, setRandomData] = useState(createData());

const onBottom = () => {
action('onBottom');
setRandomData([...randomData, ...createData(randomData.length)]);
};

return (
<Table
columns={columns}
data={randomData}
onBottom={onBottom}
onBottomOffset={5}
defaultSortingKey={'value'}
>
<Table.SingleSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
/>
</Table>
);
},
};

/* ---------------------------- MultiSelect Table --------------------------- */

export const MultiSelectTable = {
render: () => {
return (
<Table columns={columns} data={data} defaultSortingKey={'health'}>
<Table.MultiSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
onMultiSelectionChanged={action(
'Table.MultiSelectableContent selected row',
)}
/>
</Table>
);
},
};

export const MultiTable: Story = {
render: () => {
const [data1, setData1] = useState([
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
]);

const [data2, setData2] = useState([
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
]);
const columns2: Column<(typeof data2)[number]>[] = [
{
Header: 'Name',
accessor: 'name',
cellStyle: {
textAlign: 'left',
width: 'unset',
flex: 1,
},
},
{
Header: 'Volume',
accessor: 'volume',
cellStyle: {
textAlign: 'left',
width: 'unset',
flex: 1,
},
},
{
Header: 'Capacity',
accessor: 'capacity',
cellStyle: {
textAlign: 'left',
width: 'unset',
flex: 1,
},
},
];

const demo = () => {
setData1([
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
]);

setData2([
{
name: 'test',
volume: 1,
capacity: '2Gi',
},
{
name: 'test',
volume: 1,
capacity: '1Gi',
},
]);
};

return (
<Wrap justifyContent="center" gap="2rem" height={'200px'}>
<Table
columns={columns2}
data={data1}
defaultSortingKey="name"
initiallySelectedRowsIds={new Set([0, 2])}
>
<Table.MultiSelectableContent
onMultiSelectionChanged={action(
'Table.MultiSelectableContent selected row',
)}
/>
</Table>

<Stack direction="vertical" style={{ justifyContent: 'center' }}>
<Button
variant="secondary"
label=">"
onClick={() => {
demo();
}}
/>
<Button
variant="secondary"
label="<"
onClick={() => {
demo();
}}
/>
</Stack>

<Table columns={columns2} data={data2} defaultSortingKey={'health'}>
<Table.MultiSelectableContent
onMultiSelectionChanged={action(
'Table.MultiSelectableContent selected row',
)}
/>
</Table>
</Wrap>
);
},
};
631 changes: 0 additions & 631 deletions stories/tablev2.stories.tsx

This file was deleted.