diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index bfb18da0..c93634e4 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -55,6 +55,7 @@ - `Switch` - `Tabs` - `Slider` + - `DataGrid` * Supporting `tooltip` property for interactive MUI components. diff --git a/chartlets.js/package-lock.json b/chartlets.js/package-lock.json index 93de4163..05104c1d 100644 --- a/chartlets.js/package-lock.json +++ b/chartlets.js/package-lock.json @@ -1294,9 +1294,9 @@ "license": "MIT" }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.10.tgz", - "integrity": "sha512-LY5wdiLCBDY7u+Od8UmFINZFGN/5ZU90fhAslf/ZtfP+5RhuY45f679pqYIxe0y54l6Gkv9PFOc8Cs10LDTBYg==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.5.tgz", + "integrity": "sha512-zoXvHU1YuoodgMlPS+epP084Pqv9V+Vg+5IGv9n/7IIFVQ2nkTngYHYxElCq8pdTTbDcgji+nNh0lxri2abWgA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1304,22 +1304,22 @@ } }, "node_modules/@mui/material": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.10.tgz", - "integrity": "sha512-txnwYObY4N9ugv5T2n5h1KcbISegZ6l65w1/7tpSU5OB6MQCU94YkP8n/3slDw2KcEfRk4+4D8EUGfhSPMODEQ==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.5.tgz", + "integrity": "sha512-5eyEgSXocIeV1JkXs8mYyJXU0aFyXZIWI5kq2g/mCnIgJe594lkOBNAKnCIaGVfQTu2T6TTEHF8/hHIqpiIRGA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.1.10", - "@mui/system": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/core-downloads-tracker": "^6.4.5", + "@mui/system": "^6.4.3", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.3", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", + "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.3.1", + "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "engines": { @@ -1332,7 +1332,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.10", + "@mui/material-pigment-css": "^6.4.3", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1353,13 +1353,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.10.tgz", - "integrity": "sha512-DqgsH0XFEweeG3rQfVkqTkeXcj/E76PGYWag8flbPdV8IYdMo+DfVdFlZK8JEjsaIVD2Eu1kJg972XnH5pfnBQ==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.3.tgz", + "integrity": "sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.10", + "@mui/utils": "^6.4.3", "prop-types": "^15.8.1" }, "engines": { @@ -1380,9 +1380,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.10.tgz", - "integrity": "sha512-+NV9adKZYhslJ270iPjf2yzdVJwav7CIaXcMlPSi1Xy1S/zRe5xFgZ6BEoMdmGRpr34lIahE8H1acXP2myrvRw==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.3.tgz", + "integrity": "sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1414,16 +1414,16 @@ } }, "node_modules/@mui/system": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.10.tgz", - "integrity": "sha512-5YNIqxETR23SIkyP7MY2fFnXmplX/M4wNi2R+10AVRd3Ub+NLctWY/Vs5vq1oAMF0eSDLhRTGUjaUe+IGSfWqg==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.3.tgz", + "integrity": "sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.10", - "@mui/styled-engine": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/private-theming": "^6.4.3", + "@mui/styled-engine": "^6.4.3", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.3", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1454,9 +1454,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1468,17 +1468,17 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.10.tgz", - "integrity": "sha512-1ETuwswGjUiAf2dP9TkBy8p49qrw2wXa+RuAjNTRE5+91vtXJ1HKrs7H9s8CZd1zDlQVzUcUAPm9lpQwF5ogTw==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.3.tgz", + "integrity": "sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", + "@mui/types": "^7.2.21", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.0.0" }, "engines": { "node": ">=14.0.0" @@ -1497,6 +1497,66 @@ } } }, + "node_modules/@mui/x-data-grid": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.27.0.tgz", + "integrity": "sha512-RZPtAGCDF3H7+9HqSxPE+0xMtTrI9T9CRwSWiCAwrVSYUzgM4S8aQXaX1mvHgBTW9PCR0LYsFoGJWwuRRGPE/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.26.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2407,11 +2467,11 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", - "dependencies": { + "peerDependencies": { "@types/react": "*" } }, @@ -6072,9 +6132,9 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", "license": "MIT" }, "node_modules/react-transition-group": { @@ -6165,6 +6225,13 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -7083,6 +7150,16 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8202,6 +8279,7 @@ "zustand": "^5.0" }, "devDependencies": { + "@mui/x-data-grid": "^7.23.5", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/node": "^20.11.17", @@ -8224,13 +8302,17 @@ "vitest": "^2.1.8" }, "peerDependencies": { - "@mui/material": ">=6", - "react": ">=18", - "react-dom": ">=18", + "@mui/material": "^6.2.1", + "@mui/x-data-grid": ">=7", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-vega": ">=7", "vega-themes": ">=2" }, "peerDependenciesMeta": { + "@mui/x-data-grid": { + "optional": true + }, "react": { "optional": false }, diff --git a/chartlets.js/packages/lib/package.json b/chartlets.js/packages/lib/package.json index aebb3b42..153804ed 100644 --- a/chartlets.js/packages/lib/package.json +++ b/chartlets.js/packages/lib/package.json @@ -59,13 +59,17 @@ "zustand": "^5.0" }, "peerDependencies": { - "@mui/material": ">=6", - "react": ">=18", - "react-dom": ">=18", + "@mui/material": "^6.2.1", + "@mui/x-data-grid": ">=7", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-vega": ">=7", "vega-themes": ">=2" }, "peerDependenciesMeta": { + "@mui/x-data-grid": { + "optional": true + }, "react": { "optional": false }, @@ -74,6 +78,7 @@ } }, "devDependencies": { + "@mui/x-data-grid": "^7.23.5", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/node": "^20.11.17", diff --git a/chartlets.js/packages/lib/src/plugins/mui/DataGrid.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/DataGrid.test.tsx new file mode 100644 index 00000000..61c4e475 --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/DataGrid.test.tsx @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { DataGrid } from "./DataGrid"; +import { createChangeHandler } from "@/plugins/mui/common.test"; + +describe("DataGrid", () => { + const mockColumns = [ + { field: "id", headerName: "ID" }, + { field: "name", headerName: "Name" }, + ]; + const mockRows = [{ id: 1, name: "MockRow" }]; + const paginationModel = { page: 1, pageSize: 10 }; + + it("should render the DataGrid component", () => { + render( + {}} + paginationModel={paginationModel} + pageSizeOptions={[10, 25, 50]} + />, + ); + expect(screen.getByRole("grid")).toBeInTheDocument(); + expect(screen.getByText("MockRow")).toBeInTheDocument(); + + const grid = screen.getByTestId("data-grid-test-id"); + expect(grid).toHaveAttribute("aria-label", "Test DataGrid"); + }); + + it("should handle row click", () => { + const { recordedEvents, onChange } = createChangeHandler(); + render( + , + ); + + const row = screen.getByRole("row", { name: /1/ }); + fireEvent.click(row); + + expect(recordedEvents.length).toBe(1); + expect(recordedEvents[0].componentType).toBe("DataGrid"); + expect(recordedEvents[0].id).toBe("datagridId"); + expect(recordedEvents[0].property).toBe("value"); + expect(recordedEvents[0].value).toEqual(mockRows); + }); + + it("should render with other props correctly", () => { + render( + {}} + ariaLabel="Test DataGrid" + autoPageSize={true} + checkboxSelection={true} + density="compact" + disableColumnFilter={true} + loading={true} + />, + ); + + const gridRole = screen.getByTestId("data-grid-test-id"); + + // Check density + expect(gridRole).toHaveClass("MuiDataGrid-root--densityCompact"); + + // Check loading + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + + // Check checkboxSelection + expect( + screen.getByRole("columnheader", { name: /select/i }), + ).toBeInTheDocument(); + }); + + it("should not render if no columns are provided", () => { + render( + {}} + />, + ); + expect(screen.queryByRole("grid")).not.toBeInTheDocument(); + }); +}); diff --git a/chartlets.js/packages/lib/src/plugins/mui/DataGrid.tsx b/chartlets.js/packages/lib/src/plugins/mui/DataGrid.tsx new file mode 100644 index 00000000..d3cec31b --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/DataGrid.tsx @@ -0,0 +1,127 @@ +import { + DataGrid as MuiDataGrid, + type GridColDef, + type GridPaginationModel, + type GridRowModel, + type GridRowSelectionModel, +} from "@mui/x-data-grid"; +import type { ComponentProps, ComponentState } from "@/index"; + +interface DataGridState extends ComponentState { + rows?: GridRowModel[]; + columns?: GridColDef[]; + ariaLabel?: string; + autoPageSize?: boolean; + checkboxSelection?: boolean; + density?: "compact" | "standard" | "comfortable"; + disableAutosize?: boolean; + disableColumnFilter?: boolean; + disableColumnMenu?: boolean; + disableColumnResize?: boolean; + disableColumnSelector?: boolean; + disableColumnSorting?: boolean; + disableDensitySelector?: boolean; + disableMultipleRowSelection?: boolean; + disableRowSelectionOnClick?: boolean; + editMode?: "cell" | "row"; + hideFooter?: boolean; + hideFooterPagination?: boolean; + hideFooterSelectedRowCount?: boolean; + initialState?: { + pagination?: { + paginationModel: GridPaginationModel; + }; + }; + loading?: boolean; + pageSizeOptions?: number[] | { label: string; value: number }[]; + paginationModel?: GridPaginationModel; + rowHeight?: number; + rowSelection?: boolean; +} + +interface DataGridProps extends ComponentProps, DataGridState {} + +export const DataGrid = ({ + type, + id, + style, + rows, + columns, + ariaLabel, + autoPageSize, + checkboxSelection, + density, + disableAutosize, + disableColumnFilter, + disableColumnMenu, + disableColumnResize, + disableColumnSelector, + disableColumnSorting, + disableDensitySelector, + disableMultipleRowSelection, + disableRowSelectionOnClick, + editMode, + hideFooter, + hideFooterPagination, + hideFooterSelectedRowCount, + initialState, + loading, + rowHeight, + rowSelection, + paginationModel, + pageSizeOptions, + onChange, +}: DataGridProps) => { + if (!columns) { + return; + } + + const onRowsSelectionHandler = (ids: GridRowSelectionModel) => { + if (id) { + const selectedRowsData = ids.map((id) => + rows?.find((row) => row.id === id), + ); + onChange({ + componentType: type, + id: id, + property: "value", + value: selectedRowsData, + }); + } + }; + + return ( +
+ +
+ ); +}; diff --git a/chartlets.js/packages/lib/src/plugins/mui/Slider.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Slider.test.tsx index f3960a47..99280046 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Slider.test.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Slider.test.tsx @@ -28,27 +28,6 @@ describe("Slider", () => { expect(slider.getAttribute("value")).toEqual("50"); }); - it("should render the Slider component", () => { - render( - {}} - />, - ); - - const slider = screen.getByRole("slider"); - expect(slider).toBeDefined(); - - expect(slider.getAttribute("aria-orientation")).toEqual("horizontal"); - expect(slider.getAttribute("min")).toEqual("0"); - expect(slider.getAttribute("max")).toEqual("100"); - expect(slider.getAttribute("value")).toEqual("50"); - }); - it("should fire 'value' property", () => { const { recordedEvents, onChange } = createChangeHandler(); @@ -63,19 +42,18 @@ describe("Slider", () => { return ( ); }; render(); - const slider = screen.getByTestId("sliderId"); + const slider = screen.getByTestId("slider-test-id"); expect(slider).toBeInTheDocument(); expect(screen.getByRole("slider")).toHaveValue("60"); diff --git a/chartlets.js/packages/lib/src/plugins/mui/Slider.tsx b/chartlets.js/packages/lib/src/plugins/mui/Slider.tsx index b85d447d..1702c876 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Slider.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Slider.tsx @@ -22,7 +22,6 @@ interface SliderState extends ComponentState { track?: "inverted" | "normal" | false; value?: number | number[]; valueLabelDisplay?: "auto" | "on" | "off"; - ["data-testid"]?: string; } interface SliderProps extends ComponentProps, SliderState {} @@ -46,13 +45,7 @@ export const Slider = ({ value, valueLabelDisplay, onChange, - ...props }: SliderProps) => { - // We need to drop children prop because we want to access the data-testid for - // tests and slider does not accept children components - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { children: _, ...sliderProps } = props; - const handleSlide = ( _event: Event, value: number | number[], @@ -69,7 +62,6 @@ export const Slider = ({ }; return ( ); }; diff --git a/chartlets.js/packages/lib/src/plugins/mui/index.ts b/chartlets.js/packages/lib/src/plugins/mui/index.ts index 6d430bdc..0f08b348 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/index.ts +++ b/chartlets.js/packages/lib/src/plugins/mui/index.ts @@ -12,6 +12,7 @@ import { Switch } from "./Switch"; import { Tabs } from "./Tabs"; import { Typography } from "./Typography"; import { Slider } from "./Slider"; +import { DataGrid } from "@/plugins/mui/DataGrid"; export default function mui(): Plugin { return { @@ -20,6 +21,7 @@ export default function mui(): Plugin { ["Button", Button], ["Checkbox", Checkbox], ["CircularProgress", CircularProgress], + ["DataGrid", DataGrid], ["Divider", Divider], ["IconButton", IconButton], ["LinearProgress", LinearProgress], diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index d1e67280..8b9d67ac 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -30,7 +30,7 @@ - `Switch` - `Slider` - `Tabs` and `Tab` - + - `DataGrid` ## Version 0.0.29 (from 2024/11/26) diff --git a/chartlets.py/chartlets/components/__init__.py b/chartlets.py/chartlets/components/__init__.py index ac2142de..b42c2eef 100644 --- a/chartlets.py/chartlets/components/__init__.py +++ b/chartlets.py/chartlets/components/__init__.py @@ -13,6 +13,7 @@ from .select import Select from .slider import Slider from .switch import Switch +from .datagrid import DataGrid from .tabs import Tab from .tabs import Tabs from .typography import Typography diff --git a/chartlets.py/chartlets/components/datagrid.py b/chartlets.py/chartlets/components/datagrid.py new file mode 100644 index 00000000..6c3a4a7d --- /dev/null +++ b/chartlets.py/chartlets/components/datagrid.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass, field +from typing import Literal, TypedDict, Callable, List, Any + +from chartlets import Component + + +class GridPaginationModelDict(TypedDict): + page: int + pageSize: int + + +class PageSizeOption(TypedDict, total=False): + label: str + value: int + + +class GridPagination(TypedDict): + paginationModel: GridPaginationModelDict + + +class InitialState(TypedDict): + pagination: GridPagination + + +@dataclass(frozen=True) +class DataGrid(Component): + """The DataGrid presents information in a structured format of rows and + columns. + """ + + rows: List[dict[str, Any]] = field(default_factory=list) + """The data for the rows in the grid.""" + + columns: List[dict[str, Any]] = field(default_factory=list) + """The column definitions for the grid. Please have a look here to + identify the keys https://mui.com/x/api/data-grid/grid-col-def/""" + + ariaLabel: str | None = None + """The aria-label of the grid.""" + + autoPageSize: bool | None = None + """If true, the page size is automatically adjusted to fit the content.""" + + checkboxSelection: bool | None = None + """If true, checkboxes are displayed for row selection.""" + + density: Literal["compact", "standard", "comfortable"] | None = None + """The density of the grid.""" + + disableAutoSize: bool | None = None + """If true, disables autosizing of columns.""" + + disableColumnFilter: bool | None = None + """If true, disables column filtering.""" + + disableColumnMenu: bool | None = None + """If true, disables the column menu.""" + + disableColumnResize: bool | None = None + """If true, disables column resizing.""" + + disableColumnSelector: bool | None = None + """If true, disables the column selector.""" + + disableColumnSorting: bool | None = None + """If true, disables column sorting.""" + + disableDensitySelector: bool | None = None + """If true, disables the density selector.""" + + disableMultipleRowSelection: bool | None = None + """If true, disables multiple row selection.""" + + disableRowSelectionOnClick: bool | None = None + """If true, clicking on a row does not select it.""" + + editMode: Literal["cell", "row"] | None = None + """The editing mode of the grid.""" + + hideFooter: bool | None = None + """If true, hides the footer.""" + + hideFooterPagination: bool | None = None + """If true, hides the pagination in the footer.""" + + hideFooterSelectedRowCount: bool | None = None + """If true, hides the selected row count in the footer.""" + + initialState: InitialState | None = None + """The initial state of the grid, including pagination.""" + + isLoading: bool | None = None + """If true, displays a loading indicator.""" + + pageSizeOptions: list[int | PageSizeOption] | None = None + """Available page size options.""" + + paginationModel: GridPaginationModelDict | None = None + """The pagination model for the grid.""" + + rowHeight: int | None = None + """The height of each row.""" + + rowSelection: bool | None = None + """If true, row selection is enabled.""" diff --git a/chartlets.py/demo/my_extension/my_panel_4.py b/chartlets.py/demo/my_extension/my_panel_4.py index c087f04e..c17bb997 100644 --- a/chartlets.py/demo/my_extension/my_panel_4.py +++ b/chartlets.py/demo/my_extension/my_panel_4.py @@ -1,5 +1,5 @@ -from chartlets import Component, Input, State, Output -from chartlets.components import Box, Slider, Typography +from chartlets import Component, Input, Output +from chartlets.components import Box, Slider, Typography, DataGrid from server.context import Context from server.panel import Panel @@ -36,6 +36,27 @@ def render_panel( info_text = Typography(id="info_text", children=["Move the slider."]) + columns = [ + {"field": "id", "headerName": "ID"}, + {"field": "firstName", "headerName": "First Name", "width": 100}, + {"field": "lastName", "headerName": "Last Name"}, + {"field": "age", "headerName": "Age"}, + ] + + rows = [ + {"id": 1, "firstName": "John", "lastName": "Doe", "age": 30}, + {"id": 2, "firstName": "Jane", "lastName": "Smith", "age": 25}, + {"id": 3, "firstName": "Peter", "lastName": "Jones", "age": 40}, + ] + + datagrid = DataGrid( + id="datagrid", rows=rows, columns=columns, checkboxSelection=True + ) + + datagrid_text = Typography( + id="datagrid_text", children=["Click on any row in the datagrid."] + ) + return Box( style={ "display": "flex", @@ -43,16 +64,19 @@ def render_panel( "width": "100%", "height": "100%", "gap": "6px", + "padding": "15px", }, - children=[slider, info_text], + children=[slider, info_text, datagrid, datagrid_text], ) # noinspection PyUnusedLocal -@panel.callback(Input("slider"), Output("info_text", "children")) -def update_info_text( - ctx: Context, - slider: int, -) -> list[str]: +@panel.callback( + Input("slider"), + Input("datagrid"), + Output("info_text", "children"), + Output("datagrid_text", "children"), +) +def update_info_text(ctx: Context, slider: int, datagrid) -> tuple[str, str]: slider = slider or 0 - return [f"The value is {slider}."] + return f"The value is {slider}.", f"The selected row is {datagrid}." diff --git a/chartlets.py/tests/components/datagrid_test.py b/chartlets.py/tests/components/datagrid_test.py new file mode 100644 index 00000000..dbdb44c4 --- /dev/null +++ b/chartlets.py/tests/components/datagrid_test.py @@ -0,0 +1,28 @@ +from chartlets.components import DataGrid +from tests.component_test import make_base + + +class DataGridTest(make_base(DataGrid)): + def test_is_json_serializable(self): + columns = [ + {"field": "id", "headerName": "ID"}, + {"field": "firstName", "headerName": "First Name"}, + {"field": "lastName", "headerName": "Last Name"}, + {"field": "age", "headerName": "Age"}, + ] + rows = [ + {"id": 1, "firstName": "John", "lastName": "Doe", "age": 30}, + {"id": 2, "firstName": "Jane", "lastName": "Smith", "age": 25}, + ] + self.assert_is_json_serializable( + self.cls( + rows=rows, columns=columns, id="my-datagrid", checkboxSelection=True + ), + { + "type": "DataGrid", + "id": "my-datagrid", + "rows": rows, + "columns": columns, + "checkboxSelection": True, + }, + ) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8552152e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "chartlets", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}