Skip to content

Commit

Permalink
feat(ui): add MatrixGrid
Browse files Browse the repository at this point in the history
  • Loading branch information
hdinia committed Sep 5, 2024
1 parent 9f55e43 commit b60de7d
Show file tree
Hide file tree
Showing 19 changed files with 2,108 additions and 20 deletions.
1 change: 1 addition & 0 deletions webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"global.error.delete": "Deletion failed",
"global.area.add": "Add an area",
"global.add": "Add",
"global.timeSeries": "Time Series",
"login.error": "Failed to authenticate",
"tasks.title": "Tasks",
"api.title": "API",
Expand Down
1 change: 1 addition & 0 deletions webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"global.error.delete": "La suppression a échoué",
"global.area.add": "Ajouter une zone",
"global.add": "Ajouter",
"global.timeSeries": "Séries temporelles",
"login.error": "Échec de l'authentification",
"tasks.title": "Tâches",
"api.title": "API",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { useOutletContext } from "react-router";
import useAppSelector from "../../../../../../redux/hooks/useAppSelector";
import { getCurrentAreaId } from "../../../../../../redux/selectors";
import { MatrixStats, StudyMetadata } from "../../../../../../common/types";
import MatrixInput from "../../../../../common/MatrixInput";
import { Root } from "./style";
import Matrix from "../../../../../common/MatrixGrid/Matrix";

function Load() {
const { study } = useOutletContext<{ study: StudyMetadata }>();
const currentArea = useAppSelector(getCurrentAreaId);
const url = `input/load/series/load_${currentArea}`;

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Root>
<MatrixInput study={study} url={url} computStats={MatrixStats.STATS} />
</Root>
);
return <Matrix url={url} />;
}

export default Load;
84 changes: 84 additions & 0 deletions webapp/src/components/common/MatrixGrid/Matrix.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Divider, Skeleton } from "@mui/material";
import MatrixGrid from ".";
import { useMatrix } from "./useMatrix";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import ImportDialog from "../dialogs/ImportDialog";
import { useOutletContext } from "react-router";
import { StudyMetadata } from "../../../common/types";
import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style";
import MatrixActions from "./MatrixActions";

interface MatrixProps {
url: string;
title?: string;
enableTimeSeriesColumns?: boolean;
enableAggregateColumns?: boolean;
}

function Matrix({
url,
title = "global.timeSeries",
enableTimeSeriesColumns = true,
enableAggregateColumns = false,
}: MatrixProps) {
const { t } = useTranslation();
const { study } = useOutletContext<{ study: StudyMetadata }>();
const [openImportDialog, setOpenImportDialog] = useState(false);

const {
matrixData,
isLoading,
columns,
dateTime,
handleCellEdit,
handleMultipleCellsEdit,
handleImport,
} = useMatrix(study.id, url, enableTimeSeriesColumns, enableAggregateColumns);

if (isLoading || !matrixData) {
return <Skeleton sx={{ height: 1, transform: "none" }} />;
}

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<MatrixContainer>
<MatrixHeader>
<MatrixTitle>{t(title)}</MatrixTitle>
<MatrixActions
onImport={() => setOpenImportDialog(true)}
studyId={study.id}
path={url}
disabled={matrixData.data.length === 0}
/>
</MatrixHeader>

<Divider sx={{ width: 1, mt: 1, mb: 2 }} />

<MatrixGrid
data={matrixData.data}
columns={columns}
rows={matrixData.data.length}
dateTime={dateTime}
onCellEdit={handleCellEdit}
onMultipleCellsEdit={handleMultipleCellsEdit}
/>

{openImportDialog && (
<ImportDialog
open={openImportDialog}
title={t("matrix.importNewMatrix")}
dropzoneText={t("matrix.message.importHint")}
onCancel={() => setOpenImportDialog(false)}
onImport={handleImport}
accept={{ "text/*": [".csv", ".tsv", ".txt"] }}
/>
)}
</MatrixContainer>
);
}

export default Matrix;
43 changes: 43 additions & 0 deletions webapp/src/components/common/MatrixGrid/MatrixActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Box } from "@mui/material";
import SplitButton from "../buttons/SplitButton";
import DownloadMatrixButton from "../DownloadMatrixButton";
import FileDownload from "@mui/icons-material/FileDownload";
import { useTranslation } from "react-i18next";

interface MatrixActionsProps {
onImport: () => void;
studyId: string;
path: string;
disabled: boolean;
}

function MatrixActions({
onImport,
studyId,
path,
disabled,
}: MatrixActionsProps) {
const { t } = useTranslation();

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<SplitButton
options={[t("global.import.fromFile"), t("global.import.fromDatabase")]}
onClick={onImport}
size="small"
ButtonProps={{
startIcon: <FileDownload />,
}}
>
{t("global.import")}
</SplitButton>
<DownloadMatrixButton studyId={studyId} path={path} disabled={disabled} />
</Box>
);
}

export default MatrixActions;
218 changes: 218 additions & 0 deletions webapp/src/components/common/MatrixGrid/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { render } from "@testing-library/react";
import MatrixGrid, { MatrixGridProps } from ".";
import Box from "@mui/material/Box";
import { mockGetBoundingClientRect } from "../../../tests/mocks/mockGetBoundingClientRect";
import { type EnhancedGridColumn } from "./types";
import { ColumnDataType } from "./utils";

beforeEach(() => {
mockGetBoundingClientRect();
});

function renderMatrixGrid(
width: string,
height: string,
data: MatrixGridProps["data"],
columns: EnhancedGridColumn[],
rows: number,
) {
return render(
<Box style={{ width, height }}>
<MatrixGrid
data={data}
rows={rows}
columns={columns}
width="100%"
height="100%"
/>
</Box>,
);
}

function assertDimensions(
element: HTMLElement,
expectedWidth: number,
expectedHeight: number,
) {
const rect = element.getBoundingClientRect();
expect(rect.width).toBe(expectedWidth);
expect(rect.height).toBe(expectedHeight);
}

describe("MatrixGrid rendering", () => {
test("MatrixGrid should be rendered within a 450x500px container and match these dimensions", () => {
const data = [
[1, 2, 3],
[4, 5, 6],
];

const columns = [
{
id: "col1",
title: "Column 1",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 0,
},
{
id: "col2",
title: "Column 2",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 1,
},
{
id: "col3",
title: "Column 3",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 2,
},
];

const rows = 2;

// Render the MatrixGrid inside a parent container with specific dimensions
const { container } = renderMatrixGrid(
"450px", // Use inline style for exact measurement
"500px",
data,
columns,
rows,
);

const matrix = container.firstChild;

if (matrix instanceof HTMLElement) {
expect(matrix).toBeInTheDocument();
assertDimensions(matrix, 450, 500);
} else {
throw new Error("Expected an HTMLElement but received a different node.");
}
});

test("MatrixGrid should render correctly with no data", () => {
const data: MatrixGridProps["data"] = [];

const columns = [
{
id: "col1",
title: "Column 1",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 0,
},
{
id: "col2",
title: "Column 2",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 1,
},
{
id: "col3",
title: "Column 3",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 2,
},
];

const rows = 0;

const { container } = renderMatrixGrid(
"450px",
"500px",
data,
columns,
rows,
);

const matrix = container.firstChild;

if (matrix instanceof HTMLElement) {
expect(matrix).toBeInTheDocument();
assertDimensions(matrix, 450, 500);
} else {
throw new Error("Expected an HTMLElement but received a different node.");
}
});

test("MatrixGrid should match the provided dimensions when resized", () => {
const data = [
[1, 2, 3],
[4, 5, 6],
];

const columns = [
{
id: "col1",
title: "Column 1",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 0,
},
{
id: "col2",
title: "Column 2",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 1,
},
{
id: "col3",
title: "Column 3",
width: 100,
type: ColumnDataType.Number,
editable: true,
order: 2,
},
];

const rows = 2;

const { container, rerender } = renderMatrixGrid(
"450px",
"500px",
data,
columns,
rows,
);

let matrix = container.firstChild;

if (matrix instanceof HTMLElement) {
assertDimensions(matrix, 450, 500);
} else {
throw new Error("Expected an HTMLElement but received a different node.");
}

rerender(
<Box style={{ width: "300px", height: "400px" }}>
<MatrixGrid
data={data}
rows={rows}
columns={columns}
width="100%"
height="100%"
/>
</Box>,
);

matrix = container.firstChild;

if (matrix instanceof HTMLElement) {
assertDimensions(matrix, 300, 400);
} else {
throw new Error("Expected an HTMLElement but received a different node.");
}
});
});
Loading

0 comments on commit b60de7d

Please sign in to comment.