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

Add Table component #92

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions chartlets.js/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- `Switch`
- `Tabs`
- `Slider`
- `Table`

* Supporting `tooltip` property for interactive MUI components.

Expand Down
80 changes: 80 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Table } from "@/plugins/mui/Table";
import { createChangeHandler } from "@/plugins/mui/common.test";

describe("Table", () => {
const rows = [
{ id: 1, data: { firstName: "John", lastName: "Doe" } },
{ id: 2, data: { firstName: "Johnie", lastName: "Undoe" } },
];
const columns = [
{ id: "firstName", label: "First Name" },
{ id: "lastName", label: "Last Name" },
];

it("should render the Table component", () => {
render(
<Table
id="table"
type={"Table"}
rows={rows}
columns={columns}
onChange={() => {}}
/>,
);

const table = screen.getByRole("table");
expect(table).toBeDefined();
columns.forEach((column) => {
expect(screen.getByText(column.label)).toBeInTheDocument();
});
rows.forEach((row) => {
expect(screen.getByText(row.data.firstName)).toBeInTheDocument();
expect(screen.getByText(row.data.lastName)).toBeInTheDocument();
});
});

it("should not render the Table component when no columns provided", () => {
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);

const table = screen.queryByRole("table");
expect(table).toBeNull();
});

it("should not render the Table component when no rows provided", () => {
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);

const table = screen.queryByRole("table");
expect(table).toBeNull();
});

it("should call onChange on row click", () => {
const { recordedEvents, onChange } = createChangeHandler();
render(
<Table
id="table"
type={"Table"}
rows={rows}
columns={columns}
onChange={onChange}
/>,
);

fireEvent.click(screen.getAllByRole("row")[1]);
expect(recordedEvents.length).toEqual(1);
expect(recordedEvents[0]).toEqual({
componentType: "Table",
id: "table",
property: "value",
value: {
id: 1,
data: {
firstName: "John",
lastName: "Doe",
},
},
});
});
});
99 changes: 99 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
Paper,
Table as MuiTable,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import type { ComponentProps, ComponentState } from "@/index";

interface TableCellProps {
id: string | number;
size?: "medium" | "small" | string;
align?: "inherit" | "left" | "center" | "right" | "justify";
}

interface TableColumn extends TableCellProps {
label: string;
}

type row = {
[key: string]: string | number | boolean | undefined;
};

interface TableRowData extends TableCellProps {
data: row;
}

interface TableState extends ComponentState {
rows?: TableRowData[];
columns?: TableColumn[];
hover?: boolean;
}

interface TableProps extends ComponentProps, TableState {}

export const Table = ({
type,
id,
style,
rows,
columns,
hover,
onChange,
}: TableProps) => {
if (!columns || columns.length === 0) {
return <div>No columns provided.</div>;
}

if (!rows || rows.length === 0) {
return <div>No rows provided.</div>;
}

const handleRowClick = (row: TableRowData) => {
if (id) {
onChange({
componentType: type,
id: id,
property: "value",
value: row,
});
}
};

return (
<TableContainer component={Paper} style={style} id={id}>
<MuiTable>
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell key={column.id} align={column.align || "left"}>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
hover={hover}
key={row.id}
onClick={() => handleRowClick(row)}
>
{columns.map((column) => (
<TableCell
key={`${row.id}-${column.id}`}
align={column.align || "left"}
>
{row.data[column.id]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</MuiTable>
</TableContainer>
);
};
2 changes: 2 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Switch } from "./Switch";
import { Tabs } from "./Tabs";
import { Typography } from "./Typography";
import { Slider } from "./Slider";
import { Table } from "@/plugins/mui/Table";

export default function mui(): Plugin {
return {
Expand All @@ -25,6 +26,7 @@ export default function mui(): Plugin {
["Select", Select],
["Slider", Slider],
["Switch", Switch],
["Table", Table],
["Tabs", Tabs],
["Typography", Typography],
],
Expand Down
1 change: 1 addition & 0 deletions chartlets.py/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- `RadioGroup` and `Radio`
- `Tabs`
- `Slider`
- `Table`

## Version 0.0.29 (from 2024/11/26)

Expand Down
1 change: 1 addition & 0 deletions chartlets.py/chartlets/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .select import Select
from .slider import Slider
from .switch import Switch
from .table import Table
from .tabs import Tab
from .tabs import Tabs
from .typography import Typography
44 changes: 44 additions & 0 deletions chartlets.py/chartlets/components/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from dataclasses import dataclass
from typing import List, Literal, TypedDict, Optional, Dict, Union
from chartlets import Component


class TableCellProps(TypedDict, total=False):
"""Represents common properties of a table cell."""

id: str | int | float
"""The unique identifier for the cell."""

size: Literal['medium', 'small'] | str | None
"""The size of the cell."""

align: Literal["inherit", "left", "center", "right", "justify"] | None
"""The alignment of the cell content."""


class TableColumn(TableCellProps):
"""Defines a column in the table."""

label: str
"""The display label for the column header."""


class TableRowData(TableCellProps):
"""Defines a row in the table."""

data: dict[str, Union[str, int, float, bool, None]]
"""The data for the row, as a dictionary where keys are the column ids."""


@dataclass(frozen=True)
class Table(Component):
"""A basic Table with configurable rows and columns."""

columns: list[TableColumn] | None = None
"""The columns to display in the table."""

rows: list[TableRowData] | None = None
"""The rows of data to display in the table."""

hover: bool | None = None
"""A boolean indicating whether to highlight a row when hovered over"""
2 changes: 2 additions & 0 deletions chartlets.py/demo/my_extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from .my_panel_2 import panel as my_panel_2
from .my_panel_3 import panel as my_panel_3
from .my_panel_4 import panel as my_panel_4
from .my_panel_6 import panel as my_panel_6

ext = Extension(__name__)
ext.add(my_panel_1)
ext.add(my_panel_2)
ext.add(my_panel_3)
ext.add(my_panel_4)
ext.add(my_panel_6)
62 changes: 62 additions & 0 deletions chartlets.py/demo/my_extension/my_panel_6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from chartlets import Component, Input, State, Output
from chartlets.components import Box, Typography, Table

from server.context import Context
from server.panel import Panel

from chartlets.components.table import TableColumn, TableRowData

panel = Panel(__name__, title="Panel F")


# noinspection PyUnusedLocal
@panel.layout()
def render_panel(
ctx: Context,
) -> Component:
columns: list[TableColumn] = [
{"id": "id", "label": "ID"},
{"id": "firstName", "label": "First Name", "align": "left"},
{"id": "lastName", "label": "Last Name", "align": "center"},
{"id": "age", "label": "Age"},
]

rows: list[TableRowData] = [
{
"id": 1,
"data": {"id": "1", "firstName": "John", "lastName": "Doe", "age": 30},
},
{
"id": 2,
"data": {"id": "2", "firstName": "Jane", "lastName": "Smith", "age": 25},
},
{
"id": 3,
"data": {"id": "3", "firstName": "Peter", "lastName": "Jones", "age": 40},
},
]

table = Table(id="table", rows=rows, columns=columns, hover=True)

title_text = Typography(id="title_text", children=["Basic Table"])
info_text = Typography(id="info_text", children=["Click on any row."])

return Box(
style={
"display": "flex",
"flexDirection": "column",
"width": "100%",
"height": "100%",
"gap": "6px",
},
children=[title_text, table, info_text],
)


# noinspection PyUnusedLocal
@panel.callback(Input("table"), Output("info_text", "children"))
def update_info_text(
ctx: Context,
table_row: int,
) -> list[str]:
return [f"The clicked row value is {table_row}."]
41 changes: 41 additions & 0 deletions chartlets.py/tests/components/table_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from chartlets.components.table import TableColumn, TableRowData, Table
from tests.component_test import make_base


class TableTest(make_base(Table)):

def test_is_json_serializable_empty(self):
self.assert_is_json_serializable(
self.cls(),
{
"type": "Table",
},
)

columns: list[TableColumn] = [
{"id": "id", "label": "ID"},
{"id": "firstName", "label": "First Name"},
{"id": "lastName", "label": "Last Name"},
{"id": "age", "label": "Age"},
]
rows: list[TableRowData] = [
{
"id": "1",
"data": {"firstName": "John", "lastName": "Doe", "age": 30},
},
{"id": "2", "data": {"firstName": "Jane", "lastName": "Smith", "age": 25}},
{"id": 3, "data": {"firstName": "Johnie", "lastName": "Undoe", "age": 40}},
]
hover: bool = True
style = {"background-color": "lightgray", "width": "100%"}

self.assert_is_json_serializable(
self.cls(columns=columns, rows=rows, style=style, hover=hover),
{
"type": "Table",
"columns": columns,
"rows": rows,
"style": style,
"hover": hover,
},
)
Loading