Add table component

This commit is contained in:
Arnaud 2024-08-22 17:07:05 +02:00
parent d29ddc9a9e
commit 9b805d362c
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
19 changed files with 322 additions and 23 deletions

28
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lucide-react": "^0.428.0" "lucide-react": "^0.428.0",
"pretty-ms": "^9.1.0"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^1.6.1", "@chromatic-com/storybook": "^1.6.1",
@ -7690,6 +7691,17 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -7973,6 +7985,20 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/pretty-ms": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz",
"integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View File

@ -32,11 +32,11 @@
"React" "React"
], ],
"dependencies": { "dependencies": {
"lucide-react": "^0.428.0" "lucide-react": "^0.428.0",
"pretty-ms": "^9.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"@codex/sdk-js": "@codex/sdk-js#master", "@codex/sdk-js": "@codex/sdk-js#master",
"@tanstack/react-query": "^5.51.24",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },

View File

@ -10,3 +10,7 @@
display: block; display: block;
z-index: 1; z-index: 1;
} }
.document-noOverflow {
overflow: hidden;
}

View File

@ -32,6 +32,8 @@ type Props = {
size?: "normal" | "small"; size?: "normal" | "small";
center?: boolean; center?: boolean;
bold?: boolean;
}; };
export function SimpleText({ export function SimpleText({
@ -41,8 +43,9 @@ export function SimpleText({
size = "normal", size = "normal",
style, style,
children, children,
bold,
}: Props) { }: Props) {
const c = `text text--${variant} ${className} ${center ? "text--center" : ""}`; const c = `text text--${variant} ${className} ${center ? "text--center" : ""} ${bold ? "text--bold" : ""}`;
if (size === "small") { if (size === "small") {
return ( return (

View File

@ -21,3 +21,7 @@
.text--warning { .text--warning {
color: var(--codex-color-warning); color: var(--codex-color-warning);
} }
.text--bold {
font-weight: bold;
}

View File

@ -0,0 +1,13 @@
import { SimpleText } from "../SimpleText/SimpleText";
export const ActionCellRender =
(action: string, onClick: (row: string[]) => void) =>
(_: string, row: string[]) => {
return (
<a href="#" onClick={() => onClick(row)} className="cell--action">
<SimpleText variant="primary" bold={true}>
{action}
</SimpleText>
</a>
);
};

View File

@ -0,0 +1,3 @@
export const BreakCellRender = (val: string) => (
<span className="cell--break">{val || " "}</span>
);

View File

@ -0,0 +1,22 @@
.cell--break {
word-break: break-all;
}
.cell--action {
text-decoration: none;
transition: text-shadow 0.35s;
}
.cell--action:hover {
text-shadow: var(--codex-color-primary) 0px 0 20px;
}
.cell-state {
border-radius: var(--codex-border-radius);
padding: 0.5rem 0.75rem;
}
.cell-state--error {
background-color: var(--codex-color-error);
mix-blend-mode: difference;
}

View File

@ -0,0 +1,10 @@
import { ReactNode } from "react";
import "./CellRender.css";
export type CellRender = (
value: string,
row: string[],
index: number
) => ReactNode;
export const DefaultCellRender = (val: string) => val;

View File

@ -0,0 +1,3 @@
import "./CellRender.css";
export const DefaultCellRender = (val: string) => val;

View File

@ -0,0 +1,10 @@
import prettyMilliseconds from "pretty-ms";
export function DurationCellRender(value: string) {
const ms = parseInt(value, 10);
if (isNaN(ms)) {
return "Nan";
}
return prettyMilliseconds(ms, { compact: true });
}

View File

@ -0,0 +1,11 @@
type Mapping = { [k: string]: "success" | "warning" | "error" | "default" };
export const StateCellRender = (mapping: Mapping) => (value: string) => {
return (
<p>
<span className={"cell-state cell-state--" + mapping[value]}>
{value}
</span>
</p>
);
};

View File

@ -0,0 +1,34 @@
.table {
border-collapse: collapse;
width: 100%;
}
.table-container {
border: 1px solid var(--codex-border-color);
background-color: var(--codex-background-secondary);
padding: 2rem;
border-radius: var(--codex-border-radius);
overflow-x: auto;
}
.table-theadTr {
border-bottom: 1px solid var(--codex-border-color);
}
.table-theadTh {
color: var(--codex-color-light);
font-weight: normal;
text-transform: uppercase;
font-size: 0.9rem;
text-align: left;
padding: 1rem;
}
.table-tbodyTr {
border-bottom: 1px solid var(--codex-border-color);
}
.table-tbodyTd {
text-align: left;
padding: 1rem;
}

View File

@ -0,0 +1,56 @@
import { ReactNode } from "react";
import "./Table.css";
import { CellRender } from "./CellRender";
type Props = {
/**
* List of header names
*/
headers: string[];
/**
* The data are represented by a 2 dimensions array.
* Each row contains a dataset whose data structure is a string array.
*/
data: string[][];
/**
* The cell render is an array of function that returns the cell data.
*/
cells: CellRender[];
className?: string;
};
export function Table({ data, headers, cells, className }: Props) {
return (
<div className={`table-container ${className}`}>
<table className={"table"}>
<thead className="table-thead">
<tr className="table-theadTr">
{headers.map((col) => (
<th className="table-theadTh" key={col}>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, index) => (
<tr key={index} className="table-tbodyTr">
{headers.map((header, idx) => {
const render = cells[idx];
return (
<td key={header} className="table-tbodyTd">
{render(row[idx], row, index)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -1,18 +0,0 @@
export { Button } from "../components/Button/Button";
export { ButtonIcon } from "../components/ButtonIcon/ButtonIcon";
export { Input } from "../components/Input/Input";
export { InputGroup } from "../components/InputGroup/InputGroup";
export { SimpleText } from "../components/SimpleText/SimpleText";
export { Upload } from "../components/Upload/Upload";
export { Card } from "../components/Card/Card";
export { Select } from "../components/Select/Select";
export { Toast } from "../components/Toast/Toast";
export { SpaceAllocation } from "../components/SpaceAllocation/SpaceAllocation";
export { EmptyPlaceholder } from "../components/EmptyPlaceholder/EmptyPlaceholder";
export { Dropdown, type DropdownOption } from "../components/Dropdown/Dropdown";
export { Failure } from "../components/Failure/Failure";
export { Alert } from "../components/Alert/Alert";
export { Spinner } from "../components/Spinner/Spinner";
export { WebFileIcon } from "../components/WebFileIcon/WebFileIcon";
export { Stepper } from "../components/Stepper/Stepper";
export { Backdrop } from "../components/Backdrop/Backdrop";

24
src/index.ts Normal file
View File

@ -0,0 +1,24 @@
export { Button } from "./components/Button/Button";
export { ButtonIcon } from "./components/ButtonIcon/ButtonIcon";
export { Input } from "./components/Input/Input";
export { InputGroup } from "./components/InputGroup/InputGroup";
export { SimpleText } from "./components/SimpleText/SimpleText";
export { Upload } from "./components/Upload/Upload";
export { Card } from "./components/Card/Card";
export { Select } from "./components/Select/Select";
export { Toast } from "./components/Toast/Toast";
export { SpaceAllocation } from "./components/SpaceAllocation/SpaceAllocation";
export { EmptyPlaceholder } from "./components/EmptyPlaceholder/EmptyPlaceholder";
export { Dropdown, type DropdownOption } from "./components/Dropdown/Dropdown";
export { Failure } from "./components/Failure/Failure";
export { Alert } from "./components/Alert/Alert";
export { Spinner } from "./components/Spinner/Spinner";
export { WebFileIcon } from "./components/WebFileIcon/WebFileIcon";
export { Stepper } from "./components/Stepper/Stepper";
export { Backdrop } from "./components/Backdrop/Backdrop";
export { ActionCellRender } from "./components/Table/ActionCellRender";
export { BreakCellRender } from "./components/Table/BreakCellRender";
export { DefaultCellRender } from "./components/Table/CellRender";
export { DurationCellRender } from "./components/Table/DurationCellRender";
export { StateCellRender } from "./components/Table/StateCellRender";
export { Table } from "./components/Table/Table";

View File

@ -0,0 +1,3 @@
.tableSmall {
width: 350px;
}

91
stories/Table.stories.tsx Normal file
View File

@ -0,0 +1,91 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Table } from "../src/components/Table/Table";
import { DefaultCellRender } from "../src/components/Table/CellRender";
import { ActionCellRender } from "../src/components/Table/ActionCellRender";
import { BreakCellRender } from "../src/components/Table/BreakCellRender";
import "./Table.stories.css";
import { StateCellRender } from "../src/components/Table/StateCellRender";
import prettyMilliseconds from "pretty-ms";
import { DurationCellRender } from "../src/components/Table/DurationCellRender";
console.info(prettyMilliseconds(1337000000n, { compact: true }));
const meta = {
title: "Components/Table",
component: Table,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof Table>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
cells: [
DefaultCellRender,
DefaultCellRender,
DefaultCellRender,
ActionCellRender("Action", (row) => console.info(row)),
],
data: [["Ox45678FDGHJKL", "My file", "Some data"]],
headers: ["id", "title", "other", "actions"],
},
};
export const Scroll: Story = {
args: {
className: "tableSmall",
cells: [
DefaultCellRender,
DefaultCellRender,
DefaultCellRender,
ActionCellRender("Action", (row) => console.info(row)),
],
data: [["Ox45678FDGHJKLBSA21", "My file", "Some data"]],
headers: ["id", "title", "other", "actions"],
},
};
export const BreakableCell: Story = {
args: {
cells: [
BreakCellRender,
DefaultCellRender,
DefaultCellRender,
ActionCellRender("Action", (row) => console.info(row)),
],
data: [["veryverylongvaluetobreak", "My file", "Some data"]],
headers: ["break", "title", "other", "actions"],
className: "tableSmall",
},
};
export const StateCell: Story = {
args: {
cells: [
DefaultCellRender,
DefaultCellRender,
StateCellRender({ cancelled: "error" }),
ActionCellRender("Action", (row) => console.info(row)),
],
data: [["Ox45678FDGHJKL", "My file", "cancelled", "Action"]],
headers: ["id", "title", "state", "actions"],
},
};
export const DurationCell: Story = {
args: {
cells: [
DefaultCellRender,
DefaultCellRender,
DurationCellRender,
ActionCellRender("Action", (row) => console.info(row)),
],
data: [["Ox45678FDGHJKL", "My file", "3600000", "Action"]],
headers: ["id", "title", "duration", "actions"],
},
};

View File

@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta } from "@storybook/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { UploadResponse } from "@codex/sdk-js"; import { UploadResponse } from "@codex/sdk-js";
import { Upload } from "../src/components/Upload/Upload"; import { Upload } from "../src/components/Upload/Upload";