diff --git a/new/src/components/ScenarioOverview/DonutChart.tsx b/new/src/components/ScenarioOverview/DonutChart.tsx index 79fae00e..616617f5 100644 --- a/new/src/components/ScenarioOverview/DonutChart.tsx +++ b/new/src/components/ScenarioOverview/DonutChart.tsx @@ -1,31 +1,43 @@ import { ReportStatistics } from "../../reportModel"; -import { ArcElement, Chart as ChartJS, Legend, Tooltip } from "chart.js"; +import { ArcElement, BubbleDataPoint, Chart as ChartJS, Legend, Point, Tooltip } from "chart.js"; import { Doughnut } from "react-chartjs-2"; +import { useRef } from "react"; +import { ChartJSOrUndefined } from "react-chartjs-2/dist/types"; +import { useFilters } from "../../hooks/useFilters"; +import { ScenarioStatusFilter } from "./ScenarioCollectionHead"; -export function createReportCircle(props: { statistic: ReportStatistics }) { +export interface DonutChartProps { + statistic: ReportStatistics; +} + +export function DonutChart(props: DonutChartProps) { + const successLabel = "Successful:"; + const failedLabel = "Failed:"; ChartJS.register(ArcElement, Tooltip, Legend); + const { statistic } = props; + + const { setUrlSearchParams } = useFilters(); + + const chartRef = + useRef< + ChartJSOrUndefined< + "doughnut", + (number | [number, number] | Point | BubbleDataPoint | null)[], + unknown + > + >(null); + const width = 240; // set default width to 100 if none is provided via props const height = 120; // set default height to 100 if none is provided via props const data = { - labels: ["Successful:", "Failed:"], + labels: [successLabel, failedLabel], datasets: [ { - data: [props.statistic.numSuccessfulScenarios, props.statistic.numFailedScenarios], + data: [statistic.numSuccessfulScenarios, statistic.numFailedScenarios], backgroundColor: ["rgba(60, 179, 113)", "rgba(255, 0, 0)"], borderWidth: 1, - onClick: (event: MouseEvent, elements: any[], chart: any) => { - if (elements.length === 0) { - return; // user did not click on a chart element - } - const label = chart.data.labels[elements[0].index]; - if (label === "Successful") { - window.location.href = "/successful"; - } else if (label === "Failed") { - window.location.href = "/failed"; - } - }, hoverBackgroundColor: ["rgba(60,179,113,0.63)", "rgba(255,20,20,0.63)"] } ] @@ -53,5 +65,36 @@ export function createReportCircle(props: { statistic: ReportStatistics }) { } }; - return ; + const handleClick = (event: React.MouseEvent) => { + const chart = chartRef.current; + if (chart == null) { + return; + } + + const clickedElementIndex = chart.getElementsAtEventForMode( + event.nativeEvent, + "nearest", + { intersect: true }, + false + )[0].index; + + const label = chart.data.labels?.at(clickedElementIndex); + + if (label === successLabel) { + setUrlSearchParams({ status: ScenarioStatusFilter.SUCCESS }); + } else if (label === failedLabel) { + setUrlSearchParams({ status: ScenarioStatusFilter.FAILED }); + } + }; + + return ( + + ); } diff --git a/new/src/components/ScenarioOverview/ScenarioCollectionHead.tsx b/new/src/components/ScenarioOverview/ScenarioCollectionHead.tsx index deb291f3..7d3e4ae5 100644 --- a/new/src/components/ScenarioOverview/ScenarioCollectionHead.tsx +++ b/new/src/components/ScenarioOverview/ScenarioCollectionHead.tsx @@ -4,7 +4,7 @@ import RemoveIcon from "@mui/icons-material/Remove"; import AddIcon from "@mui/icons-material/Add"; import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined"; import BookmarkOutlinedIcon from "@mui/icons-material/BookmarkOutlined"; -import { createReportCircle } from "./DonutChart"; +import { DonutChart } from "./DonutChart"; import { PropsWithChildren, useMemo } from "react"; import { processWords } from "../../wordProcessor"; import { @@ -50,7 +50,7 @@ export function ScenarioCollectionHead(props: ScenarioCollectionHeadProps) { - {createReportCircle({ statistic })} + {DonutChart({ statistic })} diff --git a/new/src/components/Scenarios/__test__/StatisticsBreadcrumbs.test.tsx b/new/src/components/Scenarios/__test__/StatisticsBreadcrumbs.test.tsx index d2950814..3f732402 100644 --- a/new/src/components/Scenarios/__test__/StatisticsBreadcrumbs.test.tsx +++ b/new/src/components/Scenarios/__test__/StatisticsBreadcrumbs.test.tsx @@ -1,7 +1,20 @@ import { createReportStatistics } from "./scenarioTestData"; import { StatisticBreadcrumbs } from "../StatisticsBreadcrumbs"; import { render, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import userEvent from "@testing-library/user-event"; +import * as useFilters from "../../../hooks/useFilters"; +import { ScenarioStatusFilter } from "../../ScenarioOverview/ScenarioCollectionHead"; + +const setUrlSearchParamsMock = jest.fn(); + +beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(useFilters, "useFilters").mockReturnValue({ + filter: { status: undefined }, + setUrlSearchParams: setUrlSearchParamsMock + }); +}); describe("StatisticsBreadcrumbs", () => { it("should display statistics", () => { @@ -60,4 +73,29 @@ describe("StatisticsBreadcrumbs", () => { expect(screen.queryByText(")", { exact: false })).not.toBeInTheDocument(); } ); + + it.each([ + ["Successful", ScenarioStatusFilter.SUCCESS], + ["failed", ScenarioStatusFilter.FAILED], + ["pending", ScenarioStatusFilter.PENDING] + ])( + "Pressing %s link should filter for status %s", + (label: string, status: ScenarioStatusFilter) => { + const statistic = createReportStatistics(); + + render( + + + } /> + + + ); + + userEvent.click(screen.getByText(label, { exact: false })); + + expect(setUrlSearchParamsMock).toHaveBeenCalledWith({ + status + }); + } + ); });