diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json
index 5f7d6a9..d41e6f5 100644
--- a/public/locales/de-DE/translation.json
+++ b/public/locales/de-DE/translation.json
@@ -39,6 +39,7 @@
"deployments": "Deployments",
"batches": "Stapelverarbeitung",
"migrations": "Migrationen",
+ "reports": "Reports",
"admin": "Admin",
"help": "Hilfe",
"account": "Konto",
@@ -77,6 +78,7 @@
"decisions": "Entscheidungen",
"deployments": "Deployments",
"migrations": "Migrationen",
+ "reports": "Reports",
"admin": "Admin",
"admin-users": "Admin – Benutzer",
"admin-groups": "Admin – Gruppen",
@@ -559,6 +561,46 @@
"without-tenant-id": "Ohne Mandanten-ID"
}
},
+ "reports": {
+ "title": "Reports",
+ "subtitle": "Abgeschlossene Prozess- und Aufgabenhistorie mit Cockpit Enterprise Reports auswerten.",
+ "refresh": "Aktualisieren",
+ "run": "Report ausführen",
+ "export-csv": "CSV exportieren",
+ "export-json": "JSON exportieren",
+ "no-results": "Für die gewählten Filter wurden keine Report-Zeilen gefunden.",
+ "started-after": "Gestartet nach",
+ "started-before": "Gestartet vor",
+ "completed-after": "Abgeschlossen nach",
+ "completed-before": "Abgeschlossen vor",
+ "minimum": "Minimum",
+ "average": "Durchschnitt",
+ "maximum": "Maximum",
+ "count": "Anzahl",
+ "period": {
+ "title": "Zeitraum",
+ "month": "Monat",
+ "quarter": "Quartal",
+ "month-value": "Monat {{month}}",
+ "quarter-value": "Quartal {{quarter}}"
+ },
+ "process": {
+ "title": "Prozessinstanz-Dauer-Report",
+ "period": "Aggregation",
+ "keys": "Prozessdefinitionsschlüssel"
+ },
+ "task": {
+ "title": "Report für abgeschlossene Aufgaben",
+ "type": "Report-Typ",
+ "count": "Abgeschlossene Aufgaben",
+ "duration": "Aufgabendauer",
+ "period": "Aggregation",
+ "group-by": "Gruppieren nach",
+ "process-definition": "Prozessdefinition",
+ "task-name": "Aufgabenname",
+ "group": "Gruppe"
+ }
+ },
"admin": {
"users": "Benutzer",
"groups": "Gruppen",
diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json
index ac20fbe..5efc2df 100644
--- a/public/locales/en-US/translation.json
+++ b/public/locales/en-US/translation.json
@@ -39,6 +39,7 @@
"deployments": "Deployments",
"batches": "Batches",
"migrations": "Migrations",
+ "reports": "Reports",
"admin": "Admin",
"help": "Help",
"account": "Account",
@@ -77,6 +78,7 @@
"decisions": "Decisions",
"deployments": "Deployments",
"migrations": "Migrations",
+ "reports": "Reports",
"admin": "Admin",
"admin-users": "Admin – Users",
"admin-groups": "Admin – Groups",
@@ -559,6 +561,46 @@
"without-tenant-id": "Without Tenant ID"
}
},
+ "reports": {
+ "title": "Reports",
+ "subtitle": "Analyze completed process and task history with Cockpit Enterprise reports.",
+ "refresh": "Refresh",
+ "run": "Run report",
+ "export-csv": "Export CSV",
+ "export-json": "Export JSON",
+ "no-results": "No report rows found for the selected filters.",
+ "started-after": "Started after",
+ "started-before": "Started before",
+ "completed-after": "Completed after",
+ "completed-before": "Completed before",
+ "minimum": "Minimum",
+ "average": "Average",
+ "maximum": "Maximum",
+ "count": "Count",
+ "period": {
+ "title": "Period",
+ "month": "Month",
+ "quarter": "Quarter",
+ "month-value": "Month {{month}}",
+ "quarter-value": "Quarter {{quarter}}"
+ },
+ "process": {
+ "title": "Process Instance Duration Report",
+ "period": "Aggregation",
+ "keys": "Process definition keys"
+ },
+ "task": {
+ "title": "Completed Task Instance Report",
+ "type": "Report type",
+ "count": "Completed tasks",
+ "duration": "Task duration",
+ "period": "Aggregation",
+ "group-by": "Group by",
+ "process-definition": "Process definition",
+ "task-name": "Task name",
+ "group": "Group"
+ }
+ },
"admin": {
"users": "Users",
"groups": "Groups",
diff --git a/src/api/engine_rest.jsx b/src/api/engine_rest.jsx
index b1df2be..59df385 100644
--- a/src/api/engine_rest.jsx
+++ b/src/api/engine_rest.jsx
@@ -14,6 +14,7 @@ import deployment from "./resources/deployment.js";
import history from "./resources/history.js";
import job_definition from "./resources/job_definition.js";
import migration from "./resources/migration.js";
+import report from "./resources/report.js";
import task from "./resources/task.js";
import authorization from "./resources/authorization.js";
import decision from "./resources/decision.js";
@@ -33,6 +34,7 @@ const engine_rest = {
migration,
process_definition,
process_instance,
+ report,
task,
tenant,
user,
diff --git a/src/api/resources/report.js b/src/api/resources/report.js
new file mode 100644
index 0000000..5e4ca28
--- /dev/null
+++ b/src/api/resources/report.js
@@ -0,0 +1,38 @@
+import { GET } from "../helper.jsx";
+
+const compact_params = (params) =>
+ Object.fromEntries(
+ Object.entries(params).filter(
+ ([, value]) => value !== undefined && value !== null && value !== "",
+ ),
+ );
+
+const report_url = (path, params = {}) => {
+ const query = new URLSearchParams(compact_params(params)).toString();
+ return `${path}${query ? `?${query}` : ""}`;
+};
+
+const get_process_instance_duration = (state, params = {}) =>
+ GET(
+ report_url("/history/process-instance/report", {
+ reportType: "duration",
+ periodUnit: "month",
+ ...params,
+ }),
+ state,
+ state.api.report.process_duration,
+ );
+
+const get_task_report = (state, params = {}) =>
+ GET(
+ report_url("/history/task/report", params),
+ state,
+ state.api.report.task,
+ );
+
+const report = {
+ process_instance_duration: get_process_instance_duration,
+ task: get_task_report,
+};
+
+export default report;
diff --git a/src/api/resources/report.test.js b/src/api/resources/report.test.js
new file mode 100644
index 0000000..bd8f69f
--- /dev/null
+++ b/src/api/resources/report.test.js
@@ -0,0 +1,65 @@
+import { describe, it, vi, beforeEach } from "vitest";
+
+vi.mock("../helper.jsx", () => ({
+ GET: vi.fn(),
+}));
+
+import { GET } from "../helper.jsx";
+import { create_mock_state, expect_api_call } from "../../test/helpers.js";
+import report from "./report.js";
+
+describe("api/resources/report", () => {
+ let state;
+
+ beforeEach(() => {
+ state = create_mock_state();
+ });
+
+ it("process_instance_duration() GETs the duration report with defaults", () => {
+ report.process_instance_duration(state);
+ expect_api_call(GET, {
+ url: "/history/process-instance/report?reportType=duration&periodUnit=month",
+ state,
+ signal: state.api.report.process_duration,
+ });
+ });
+
+ it("process_instance_duration() appends filters", () => {
+ report.process_instance_duration(state, {
+ periodUnit: "quarter",
+ processDefinitionKeyIn: "invoice",
+ startedAfter: "2026-01-01T00:00:00.000+0000",
+ startedBefore: "2026-12-31T23:59:59.999+0000",
+ });
+ expect_api_call(GET, {
+ url: "/history/process-instance/report?reportType=duration&periodUnit=quarter&processDefinitionKeyIn=invoice&startedAfter=2026-01-01T00%3A00%3A00.000%2B0000&startedBefore=2026-12-31T23%3A59%3A59.999%2B0000",
+ state,
+ signal: state.api.report.process_duration,
+ });
+ });
+
+ it("task() GETs the historic task count report", () => {
+ report.task(state, {
+ reportType: "count",
+ groupBy: "processDefinition",
+ completedAfter: "2026-01-01T00:00:00.000+0000",
+ });
+ expect_api_call(GET, {
+ url: "/history/task/report?reportType=count&groupBy=processDefinition&completedAfter=2026-01-01T00%3A00%3A00.000%2B0000",
+ state,
+ signal: state.api.report.task,
+ });
+ });
+
+ it("task() GETs the historic task duration report", () => {
+ report.task(state, {
+ reportType: "duration",
+ periodUnit: "quarter",
+ });
+ expect_api_call(GET, {
+ url: "/history/task/report?reportType=duration&periodUnit=quarter",
+ state,
+ signal: state.api.report.task,
+ });
+ });
+});
diff --git a/src/components/GoTo.jsx b/src/components/GoTo.jsx
index 87dde84..bdd79f4 100644
--- a/src/components/GoTo.jsx
+++ b/src/components/GoTo.jsx
@@ -30,6 +30,7 @@ const PAGES = [
{ nameKey: "goto.pages.decisions", href: "/decisions" },
{ nameKey: "goto.pages.deployments", href: "/deployments" },
{ nameKey: "goto.pages.migrations", href: "/migrations" },
+ { nameKey: "goto.pages.reports", href: "/reports" },
{ nameKey: "goto.pages.admin", href: "/admin" },
{ nameKey: "goto.pages.admin-users", href: "/admin/users" },
{ nameKey: "goto.pages.admin-groups", href: "/admin/groups" },
diff --git a/src/components/GoTo.test.jsx b/src/components/GoTo.test.jsx
index 2ddf3d2..c4d1caa 100644
--- a/src/components/GoTo.test.jsx
+++ b/src/components/GoTo.test.jsx
@@ -46,6 +46,12 @@ describe("GoTo", () => {
const items = Array.from(container.querySelectorAll(".goto-item"));
const hrefs = items.map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/tasks");
+ type(input, "reports");
+ expect(
+ Array.from(container.querySelectorAll(".goto-item")).map((a) =>
+ a.getAttribute("href"),
+ ),
+ ).toContain("/reports");
// A non-matching query should yield no page entries.
type(input, "zzzznotarealpage");
expect(
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
index ce108ea..e772438 100644
--- a/src/components/Header.jsx
+++ b/src/components/Header.jsx
@@ -67,6 +67,7 @@ export function Header() {
{t("nav.deployments")}
{t("nav.batches")}
{t("nav.migrations")}
+ {t("nav.reports")}
{t("nav.admin")}
@@ -146,6 +147,11 @@ export function Header() {
{t("nav.migrations")}
+
+
+ {t("nav.reports")}
+
+
{t("nav.admin")}
diff --git a/src/components/Header.test.jsx b/src/components/Header.test.jsx
index a5f18b3..63fe8ae 100644
--- a/src/components/Header.test.jsx
+++ b/src/components/Header.test.jsx
@@ -61,7 +61,7 @@ describe("Header", () => {
a.getAttribute("href"),
);
// Logo (/), tasks, processes, decisions, deployments, batches,
- // migrations, admin.
+ // migrations, reports, admin.
expect(hrefs).toEqual([
"/",
"/tasks",
@@ -70,6 +70,7 @@ describe("Header", () => {
"/deployments",
"/batches",
"/migrations",
+ "/reports",
"/admin",
]);
});
diff --git a/src/css/style.css b/src/css/style.css
index 59fa993..e60302d 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -828,6 +828,57 @@
text-align: center;
}
+ .reports {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-2);
+ height: 100%;
+ padding: var(--spacing-2);
+ }
+ .reports > header,
+ .reports > section > header {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: start;
+ gap: var(--spacing-2);
+ }
+ .reports > header p {
+ color: var(--text-2);
+ margin: 0;
+ }
+ .reports > section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-1);
+ }
+ .reports-filter {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ gap: var(--spacing-1);
+ }
+ .reports-filter label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ min-width: 12rem;
+ }
+ .reports-filter input,
+ .reports-filter select {
+ width: 100%;
+ }
+ .reports-filter button {
+ margin: 0;
+ }
+ .reports table {
+ width: 100%;
+ }
+ .reports :where(td, th) {
+ vertical-align: top;
+ }
+
#admin-page {
display: flex;
flex: 1;
diff --git a/src/index.jsx b/src/index.jsx
index 075d533..7ba7272 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -15,6 +15,7 @@ import { MigrationsPage } from "./pages/Migrations.jsx";
import { AdminPage } from "./pages/Admin.jsx";
import { DeploymentsPage } from "./pages/Deployments.jsx";
import { BatchesPage } from "./pages/Batches.jsx";
+import { ReportsPage } from "./pages/Reports.jsx";
import { NotFound } from "./pages/_404.jsx";
import { AccountPage } from "./pages/Account.jsx";
@@ -90,6 +91,7 @@ const Routing = () => {
component={DeploymentsPage}
/>
+
new Date().toISOString().slice(0, 10);
+
+const start_of_year = () => {
+ const now = new Date();
+ return `${now.getFullYear()}-01-01`;
+};
+
+const date_param = (value, end = false) =>
+ value ? `${value}T${end ? "23:59:59.999" : "00:00:00.000"}+0000` : null;
+
+const process_report_params = (form) => ({
+ periodUnit: form.period_unit.value,
+ processDefinitionKeyIn: form.process_definition_keys.value,
+ startedAfter: date_param(form.started_after.value),
+ startedBefore: date_param(form.started_before.value, true),
+});
+
+const task_report_params = (form) => ({
+ reportType: form.report_type.value,
+ periodUnit:
+ form.report_type.value === "duration" ? form.period_unit.value : null,
+ groupBy: form.report_type.value === "count" ? form.group_by.value : null,
+ completedAfter: date_param(form.completed_after.value),
+ completedBefore: date_param(form.completed_before.value, true),
+});
+
+const load_reports = (state, process_form, task_form) => {
+ void engine_rest.report.process_instance_duration(
+ state,
+ process_report_params(process_form),
+ );
+ void engine_rest.report.task(state, task_report_params(task_form));
+};
+
+const num = (value) => (value ?? 0).toLocaleString();
+
+const format_duration = (value) => {
+ if (value === null || value === undefined) return "-";
+ const total_seconds = Math.round(Number(value) / 1000),
+ days = Math.floor(total_seconds / 86400),
+ hours = Math.floor((total_seconds % 86400) / 3600),
+ minutes = Math.floor((total_seconds % 3600) / 60),
+ seconds = total_seconds % 60;
+
+ if (days > 0) return `${days}d ${hours}h`;
+ if (hours > 0) return `${hours}h ${minutes}m`;
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
+ return `${seconds}s`;
+};
+
+const csv_escape = (value) => {
+ const text = value === null || value === undefined ? "" : String(value);
+ return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
+};
+
+const download_text = (filename, type, content) => {
+ const url = URL.createObjectURL(new Blob([content], { type })),
+ link = document.createElement("a");
+ link.href = url;
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(url);
+};
+
+const export_json = (filename, rows) =>
+ download_text(
+ filename,
+ "application/json",
+ JSON.stringify(rows ?? [], null, 2),
+ );
+
+const export_csv = (filename, rows, columns) => {
+ const csv = [
+ columns.map(({ label }) => csv_escape(label)).join(","),
+ ...(rows ?? []).map((row) =>
+ columns.map(({ value }) => csv_escape(value(row))).join(","),
+ ),
+ ].join("\n");
+ download_text(filename, "text/csv", csv);
+};
+
+const ReportsPage = () => {
+ const state = useContext(AppState),
+ [t] = useTranslation(),
+ process_form = {
+ period_unit: useSignal("month"),
+ process_definition_keys: useSignal(""),
+ started_after: useSignal(start_of_year()),
+ started_before: useSignal(today()),
+ },
+ task_form = {
+ report_type: useSignal("count"),
+ period_unit: useSignal("month"),
+ group_by: useSignal("processDefinition"),
+ completed_after: useSignal(start_of_year()),
+ completed_before: useSignal(today()),
+ };
+
+ useEffect(() => {
+ load_reports(state, process_form, task_form);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+
{t("reports.title")}
+
{t("reports.subtitle")}
+
+
+
+
+
+
+
+ );
+};
+
+const ProcessDurationReport = ({ form }) => {
+ const state = useContext(AppState),
+ [t] = useTranslation(),
+ signal = state.api.report.process_duration,
+ run = (event) => {
+ event.preventDefault();
+ void engine_rest.report.process_instance_duration(
+ state,
+ process_report_params(form),
+ );
+ };
+
+ return (
+
+
+ {t("reports.process.title")}
+
+
+
+ }
+ />
+
+ );
+};
+
+const TaskReport = ({ form }) => {
+ const state = useContext(AppState),
+ [t] = useTranslation(),
+ signal = state.api.report.task,
+ run = (event) => {
+ event.preventDefault();
+ void engine_rest.report.task(state, task_report_params(form));
+ };
+
+ return (
+
+
+ {t("reports.task.title")}
+
+
+
+
+ form.report_type.value === "duration" ? (
+
+ ) : (
+
+ )
+ }
+ />
+
+ );
+};
+
+const ExportButtons = ({ filename, signal, columns }) => {
+ const [t] = useTranslation(),
+ rows = signal.value?.data ?? [],
+ disabled = rows.length === 0;
+
+ return (
+
+
+
+
+ );
+};
+
+const DurationTable = ({ rows }) => {
+ const [t] = useTranslation();
+ if (rows.length === 0)
+ return {t("reports.no-results")}
;
+
+ return (
+
+
+
+ | {t("reports.period.title")} |
+ {t("reports.minimum")} |
+ {t("reports.average")} |
+ {t("reports.maximum")} |
+
+
+
+ {rows.map((row) => (
+
+ | {format_period(row, t)} |
+ {format_duration(row.minimum)} |
+ {format_duration(row.average)} |
+ {format_duration(row.maximum)} |
+
+ ))}
+
+
+ );
+};
+
+const TaskCountTable = ({ rows }) => {
+ const [t] = useTranslation();
+ if (rows.length === 0)
+ return {t("reports.no-results")}
;
+
+ return (
+
+
+
+ | {t("reports.task.group")} |
+ {t("reports.task.process-definition")} |
+ {t("common.key")} |
+ {t("reports.count")} |
+
+
+
+ {rows.map((row) => (
+
+ |
+ {row.taskName ??
+ row.taskDefinitionKey ??
+ row.processDefinitionName ??
+ "-"}
+ |
+ {row.processDefinitionName ?? row.processDefinitionId ?? "-"} |
+ {row.processDefinitionKey ?? "-"} |
+ {num(row.count)} |
+
+ ))}
+
+
+ );
+};
+
+const format_period = (row, t) => {
+ const unit = String(row.periodUnit ?? "").toLowerCase();
+ if (unit === "quarter")
+ return t("reports.period.quarter-value", { quarter: row.period });
+ return t("reports.period.month-value", { month: row.period });
+};
+
+const duration_columns = (t) => [
+ { label: t("reports.period.title"), value: (row) => format_period(row, t) },
+ { label: t("reports.minimum"), value: (row) => row.minimum },
+ { label: t("reports.average"), value: (row) => row.average },
+ { label: t("reports.maximum"), value: (row) => row.maximum },
+];
+
+const task_count_columns = (t) => [
+ {
+ label: t("reports.task.group"),
+ value: (row) => row.taskName ?? row.taskDefinitionKey ?? "",
+ },
+ {
+ label: t("reports.task.process-definition"),
+ value: (row) => row.processDefinitionName ?? row.processDefinitionId ?? "",
+ },
+ { label: t("common.key"), value: (row) => row.processDefinitionKey ?? "" },
+ { label: t("reports.count"), value: (row) => row.count },
+];
+
+export { ReportsPage };
diff --git a/src/pages/Reports.test.jsx b/src/pages/Reports.test.jsx
new file mode 100644
index 0000000..3ea6fea
--- /dev/null
+++ b/src/pages/Reports.test.jsx
@@ -0,0 +1,121 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { h } from "preact";
+import { render, cleanup, fireEvent, waitFor } from "@testing-library/preact";
+
+vi.mock("../api/engine_rest.jsx", async (importOriginal) => {
+ const actual = await importOriginal();
+ const spyify = (o) =>
+ Object.fromEntries(
+ Object.entries(o).map(([k, v]) => [
+ k,
+ typeof v === "function"
+ ? vi.fn()
+ : v && typeof v === "object"
+ ? spyify(v)
+ : v,
+ ]),
+ );
+ return { ...actual, default: spyify(actual.default) };
+});
+
+import { AppState } from "../state.js";
+import engine_rest from "../api/engine_rest.jsx";
+import { ReportsPage } from "./Reports.jsx";
+import { create_mock_state, signal_response } from "../test/helpers.js";
+
+const renderPage = (state) =>
+ render(h(AppState.Provider, { value: state }, h(ReportsPage, {})));
+
+const populate_reports = (state) => {
+ signal_response(state.api.report.process_duration, [
+ { period: 1, periodUnit: "MONTH", minimum: 1000, average: 2000, maximum: 3000 },
+ ]);
+ signal_response(state.api.report.task, [
+ {
+ taskName: "Approve invoice",
+ taskDefinitionKey: "approve",
+ processDefinitionId: "invoice:1",
+ processDefinitionKey: "invoice",
+ processDefinitionName: "Invoice",
+ count: 42,
+ },
+ ]);
+};
+
+describe("ReportsPage", () => {
+ let state;
+
+ beforeEach(() => {
+ state = create_mock_state();
+ });
+
+ afterEach(cleanup);
+
+ it("fetches process duration and task reports on mount", () => {
+ renderPage(state);
+
+ expect(engine_rest.report.process_instance_duration).toHaveBeenCalled();
+ expect(engine_rest.report.task).toHaveBeenCalled();
+ expect(engine_rest.report.process_instance_duration.mock.lastCall[0]).toBe(
+ state,
+ );
+ expect(engine_rest.report.task.mock.lastCall[0]).toBe(state);
+ expect(engine_rest.report.task.mock.lastCall[1]).toMatchObject({
+ reportType: "count",
+ groupBy: "processDefinition",
+ });
+ });
+
+ it("renders duration and completed task report rows", () => {
+ populate_reports(state);
+
+ const { getByText } = renderPage(state);
+
+ expect(getByText("1s")).toBeTruthy();
+ expect(getByText("2s")).toBeTruthy();
+ expect(getByText("3s")).toBeTruthy();
+ expect(getByText("Approve invoice")).toBeTruthy();
+ expect(getByText("Invoice")).toBeTruthy();
+ expect(getByText("42")).toBeTruthy();
+ });
+
+ it("runs the process duration report with filters", async () => {
+ populate_reports(state);
+
+ const { container, getAllByText } = renderPage(state);
+ const process_section = container.querySelector(".reports > section");
+ const inputs = process_section.querySelectorAll("input");
+ fireEvent.input(inputs[0], { target: { value: "invoice" } });
+ fireEvent.input(inputs[1], { target: { value: "2026-01-01" } });
+ fireEvent.input(inputs[2], { target: { value: "2026-06-16" } });
+ fireEvent.submit(getAllByText("reports.run")[0].closest("form"));
+
+ await waitFor(() =>
+ expect(engine_rest.report.process_instance_duration).toHaveBeenCalled(),
+ );
+ const params =
+ engine_rest.report.process_instance_duration.mock.lastCall[1];
+ expect(params).toMatchObject({
+ processDefinitionKeyIn: "invoice",
+ startedAfter: "2026-01-01T00:00:00.000+0000",
+ startedBefore: "2026-06-16T23:59:59.999+0000",
+ });
+ });
+
+ it("runs the task duration report with period aggregation", async () => {
+ populate_reports(state);
+
+ const { container, getAllByText } = renderPage(state);
+ const task_section = container.querySelectorAll(".reports > section")[1];
+ fireEvent.change(task_section.querySelector("select"), {
+ target: { value: "duration" },
+ });
+ fireEvent.submit(getAllByText("reports.run")[1].closest("form"));
+
+ await waitFor(() => expect(engine_rest.report.task).toHaveBeenCalled());
+ expect(engine_rest.report.task.mock.lastCall[1]).toMatchObject({
+ reportType: "duration",
+ periodUnit: "month",
+ });
+ });
+});
diff --git a/src/state.js b/src/state.js
index f63ad2e..c218711 100644
--- a/src/state.js
+++ b/src/state.js
@@ -84,6 +84,10 @@ const createAppState = () => {
update: signal(null),
saved_filters: signal(null),
},
+ report: {
+ process_duration: signal(null),
+ task: signal(null),
+ },
tenant: {
list: signal(null),
by_member: signal(null),
diff --git a/src/state.test.js b/src/state.test.js
index c6b9968..4327958 100644
--- a/src/state.test.js
+++ b/src/state.test.js
@@ -57,6 +57,8 @@ describe("state", () => {
expect(api.authorization.all.value).toBeNull();
expect(api.batch.list.value).toBeNull();
expect(api.batch.one.value).toBeNull();
+ expect(api.report.process_duration.value).toBeNull();
+ expect(api.report.task.value).toBeNull();
});
});
});