Merge pull request #5 from codex-storage/table-component
Table component
This commit is contained in:
commit
b99008ba3d
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,3 +10,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-noOverflow {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -21,3 +21,7 @@
|
||||||
.text--warning {
|
.text--warning {
|
||||||
color: var(--codex-color-warning);
|
color: var(--codex-color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text--bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { SimpleText } from "../SimpleText/SimpleText";
|
||||||
|
|
||||||
|
export const ActionCellRender =
|
||||||
|
(action: string, onClick: (row: string[]) => void) =>
|
||||||
|
(_: string, row: string[]) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(row);
|
||||||
|
}}
|
||||||
|
className="cell--action"
|
||||||
|
>
|
||||||
|
<SimpleText variant="primary" bold={true}>
|
||||||
|
{action}
|
||||||
|
</SimpleText>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const BreakCellRender = (val: string) => (
|
||||||
|
<span className="cell--break">{val || " "}</span>
|
||||||
|
);
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import "./CellRender.css";
|
||||||
|
|
||||||
|
export const DefaultCellRender = (val: string) => val;
|
|
@ -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 });
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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";
|
|
|
@ -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";
|
|
@ -0,0 +1,3 @@
|
||||||
|
.tableSmall {
|
||||||
|
width: 350px;
|
||||||
|
}
|
|
@ -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"],
|
||||||
|
},
|
||||||
|
};
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue