Skip to content

Commit 8a22ad7

Browse files
b-yogeshforman
andauthored
Add Table component (#92)
* Implement Table.tsx * Implement table.py * add table * add tests * add demo * update CHANGES.md * [WIP] - update table component * Finish update table component * update TableRow type def syntax * update CHANGES.md * fix linting * Update chartlets.py/chartlets/components/table.py Co-authored-by: Norman Fomferra <[email protected]> * add typealias import --------- Co-authored-by: Norman Fomferra <[email protected]>
1 parent 49014f4 commit 8a22ad7

File tree

10 files changed

+332
-0
lines changed

10 files changed

+332
-0
lines changed

chartlets.js/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* New (MUI) components
77
- `DataGrid`
88
- `Dialog`
9+
- `Table`
910

1011
## Version 0.1.3 (from 2025/01/28)
1112

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from "vitest";
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import "@testing-library/jest-dom";
4+
import { Table } from "@/plugins/mui/Table";
5+
import { createChangeHandler } from "@/plugins/mui/common.test";
6+
7+
describe("Table", () => {
8+
const rows = [
9+
["John", "Doe"],
10+
["Johnie", "Undoe"],
11+
];
12+
const columns = [
13+
{ id: "firstName", label: "First Name" },
14+
{ id: "lastName", label: "Last Name" },
15+
];
16+
17+
it("should render the Table component", () => {
18+
render(
19+
<Table
20+
id="table"
21+
type={"Table"}
22+
rows={rows}
23+
columns={columns}
24+
onChange={() => {}}
25+
/>,
26+
);
27+
28+
const table = screen.getByRole("table");
29+
expect(table).toBeDefined();
30+
columns.forEach((column) => {
31+
expect(screen.getByText(column.label)).toBeInTheDocument();
32+
});
33+
rows.forEach((row, index) => {
34+
expect(screen.getByText(row[index])).toBeInTheDocument();
35+
});
36+
});
37+
38+
it("should not render the Table component when no columns provided", () => {
39+
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);
40+
41+
const table = screen.queryByRole("table");
42+
expect(table).toBeNull();
43+
});
44+
45+
it("should not render the Table component when no rows provided", () => {
46+
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);
47+
48+
const table = screen.queryByRole("table");
49+
expect(table).toBeNull();
50+
});
51+
52+
it("should call onChange on row click", () => {
53+
const { recordedEvents, onChange } = createChangeHandler();
54+
render(
55+
<Table
56+
id="table"
57+
type={"Table"}
58+
rows={rows}
59+
columns={columns}
60+
onChange={onChange}
61+
/>,
62+
);
63+
64+
fireEvent.click(screen.getAllByRole("row")[1]);
65+
expect(recordedEvents.length).toEqual(1);
66+
expect(recordedEvents[0]).toEqual({
67+
componentType: "Table",
68+
id: "table",
69+
property: "value",
70+
value: {
71+
firstName: "John",
72+
lastName: "Doe",
73+
},
74+
});
75+
});
76+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
Paper,
3+
Table as MuiTable,
4+
TableBody,
5+
TableCell,
6+
TableContainer,
7+
TableHead,
8+
TableRow,
9+
} from "@mui/material";
10+
import type { ComponentProps, ComponentState } from "@/index";
11+
import type { SxProps } from "@mui/system";
12+
13+
interface TableCellProps {
14+
id: string | number;
15+
size?: "medium" | "small";
16+
align?: "inherit" | "left" | "center" | "right" | "justify";
17+
sx?: SxProps;
18+
}
19+
20+
interface TableColumn extends TableCellProps {
21+
label: string;
22+
}
23+
24+
interface TableState extends ComponentState {
25+
rows?: (string | number | boolean | undefined)[][];
26+
columns?: TableColumn[];
27+
hover?: boolean;
28+
stickyHeader?: boolean;
29+
}
30+
31+
interface TableProps extends ComponentProps, TableState {}
32+
33+
export const Table = ({
34+
type,
35+
id,
36+
style,
37+
rows,
38+
columns,
39+
hover,
40+
stickyHeader,
41+
onChange,
42+
}: TableProps) => {
43+
if (!columns || columns.length === 0) {
44+
return <div>No columns provided.</div>;
45+
}
46+
47+
if (!rows || rows.length === 0) {
48+
return <div>No rows provided.</div>;
49+
}
50+
51+
const handleRowClick = (row: (string | number | boolean | undefined)[]) => {
52+
const rowData = row.reduce(
53+
(acc, cell, cellIndex) => {
54+
const columnId = columns[cellIndex]?.id;
55+
if (columnId) {
56+
acc[columnId] = cell;
57+
}
58+
return acc;
59+
},
60+
{} as Record<string, string | number | boolean | undefined>,
61+
);
62+
if (id) {
63+
onChange({
64+
componentType: type,
65+
id: id,
66+
property: "value",
67+
value: rowData,
68+
});
69+
}
70+
};
71+
72+
return (
73+
<TableContainer component={Paper} sx={style} id={id}>
74+
<MuiTable stickyHeader={stickyHeader}>
75+
<TableHead>
76+
<TableRow>
77+
{columns.map((column) => (
78+
<TableCell
79+
key={column.id}
80+
align={column.align || "inherit"}
81+
size={column.size || "medium"}
82+
>
83+
{column.label}
84+
</TableCell>
85+
))}
86+
</TableRow>
87+
</TableHead>
88+
<TableBody>
89+
{rows.map((row, row_index) => (
90+
<TableRow
91+
hover={hover}
92+
key={row_index}
93+
onClick={() => handleRowClick(row)}
94+
>
95+
{row?.map((item, item_index) => (
96+
<TableCell
97+
key={item_index}
98+
align={columns[item_index].align || "inherit"}
99+
size={columns[item_index].size || "medium"}
100+
>
101+
{item}
102+
</TableCell>
103+
))}
104+
</TableRow>
105+
))}
106+
</TableBody>
107+
</MuiTable>
108+
</TableContainer>
109+
);
110+
};

chartlets.js/packages/lib/src/plugins/mui/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Typography } from "./Typography";
1414
import { Slider } from "./Slider";
1515
import { DataGrid } from "@/plugins/mui/DataGrid";
1616
import { Dialog } from "@/plugins/mui/Dialog";
17+
import { Table } from "@/plugins/mui/Table";
1718

1819
export default function mui(): Plugin {
1920
return {
@@ -31,6 +32,7 @@ export default function mui(): Plugin {
3132
["Select", Select],
3233
["Slider", Slider],
3334
["Switch", Switch],
35+
["Table", Table],
3436
["Tabs", Tabs],
3537
["Typography", Typography],
3638
],

chartlets.py/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* New (MUI) components
44
- `DataGrid`
55
- `Dialog`
6+
- `Table`
67

78

89
## Version 0.1.3 (from 2025/01/28)

chartlets.py/chartlets/components/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .slider import Slider
1616
from .switch import Switch
1717
from .datagrid import DataGrid
18+
from .table import Table
1819
from .tabs import Tab
1920
from .tabs import Tabs
2021
from .typography import Typography
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from dataclasses import dataclass
2+
from typing import Literal, TypedDict, TypeAlias
3+
from chartlets import Component
4+
5+
6+
class TableCellProps(TypedDict, total=False):
7+
"""Represents common properties of a table cell."""
8+
9+
id: str | int | float
10+
"""The unique identifier for the cell."""
11+
12+
size: Literal['medium', 'small'] | str | None
13+
"""The size of the cell."""
14+
15+
align: Literal["inherit", "left", "center", "right", "justify"] | None
16+
"""The alignment of the cell content."""
17+
18+
19+
class TableColumn(TableCellProps):
20+
"""Defines a column in the table."""
21+
22+
label: str
23+
"""The display label for the column header."""
24+
25+
26+
TableRow: TypeAlias = list[list[str | int | float | bool | None]]
27+
28+
29+
@dataclass(frozen=True)
30+
class Table(Component):
31+
"""A basic Table with configurable rows and columns."""
32+
33+
columns: list[TableColumn] | None = None
34+
"""The columns to display in the table."""
35+
36+
rows: TableRow | None = None
37+
"""The rows of data to display in the table."""
38+
39+
hover: bool | None = None
40+
"""A boolean indicating whether to highlight a row when hovered over"""
41+
42+
stickyHeader: bool | None = None
43+
"""A boolean to set the header of the table sticky"""

chartlets.py/demo/my_extension/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from .my_panel_3 import panel as my_panel_3
55
from .my_panel_4 import panel as my_panel_4
66
from .my_panel_5 import panel as my_panel_5
7+
from .my_panel_6 import panel as my_panel_6
78

89
ext = Extension(__name__)
910
ext.add(my_panel_1)
1011
ext.add(my_panel_2)
1112
ext.add(my_panel_3)
1213
ext.add(my_panel_4)
1314
ext.add(my_panel_5)
15+
ext.add(my_panel_6)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from chartlets import Component, Input, Output
2+
from chartlets.components import Box, Typography, Table
3+
4+
from server.context import Context
5+
from server.panel import Panel
6+
7+
from chartlets.components.table import TableColumn, TableRow
8+
9+
panel = Panel(__name__, title="Panel F")
10+
11+
12+
# noinspection PyUnusedLocal
13+
@panel.layout()
14+
def render_panel(
15+
ctx: Context,
16+
) -> Component:
17+
columns: list[TableColumn] = [
18+
{"id": "id", "label": "ID", "sortDirection": "desc"},
19+
{
20+
"id": "firstName",
21+
"label": "First Name",
22+
"align": "left",
23+
"sortDirection": "desc",
24+
},
25+
{"id": "lastName", "label": "Last Name", "align": "center"},
26+
{"id": "age", "label": "Age"},
27+
]
28+
29+
rows: TableRow = [
30+
["1", "John", "Doe", 30],
31+
["2", "Jane", "Smith", 25],
32+
["3", "Peter", "Jones", 40],
33+
]
34+
35+
table = Table(id="table", rows=rows, columns=columns, hover=True)
36+
37+
title_text = Typography(id="title_text", children=["Basic Table"])
38+
info_text = Typography(id="info_text", children=["Click on any row."])
39+
40+
return Box(
41+
style={
42+
"display": "flex",
43+
"flexDirection": "column",
44+
"width": "100%",
45+
"height": "100%",
46+
"gap": "6px",
47+
},
48+
children=[title_text, table, info_text],
49+
)
50+
51+
52+
# noinspection PyUnusedLocal
53+
@panel.callback(Input("table"), Output("info_text", "children"))
54+
def update_info_text(
55+
ctx: Context,
56+
table_row: int,
57+
) -> list[str]:
58+
return [f"The clicked row value is {table_row}."]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from chartlets.components.table import TableColumn, Table, TableRow
2+
from tests.component_test import make_base
3+
4+
5+
class TableTest(make_base(Table)):
6+
7+
def test_is_json_serializable_empty(self):
8+
self.assert_is_json_serializable(
9+
self.cls(),
10+
{
11+
"type": "Table",
12+
},
13+
)
14+
15+
columns: list[TableColumn] = [
16+
{"id": "id", "label": "ID"},
17+
{"id": "firstName", "label": "First Name"},
18+
{"id": "lastName", "label": "Last Name"},
19+
{"id": "age", "label": "Age"},
20+
]
21+
rows: TableRow = [
22+
["John", "Doe", 30],
23+
["Jane", "Smith", 25],
24+
["Johnie", "Undoe", 40],
25+
]
26+
hover: bool = True
27+
style = {"background-color": "lightgray", "width": "100%"}
28+
29+
self.assert_is_json_serializable(
30+
self.cls(columns=columns, rows=rows, style=style, hover=hover),
31+
{
32+
"type": "Table",
33+
"columns": columns,
34+
"rows": rows,
35+
"style": style,
36+
"hover": hover,
37+
},
38+
)

0 commit comments

Comments
 (0)