Skip to content
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
42 changes: 42 additions & 0 deletions public/locales/de-DE/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"deployments": "Deployments",
"batches": "Stapelverarbeitung",
"migrations": "Migrationen",
"reports": "Reports",
"admin": "Admin",
"help": "Hilfe",
"account": "Konto",
Expand Down Expand Up @@ -77,6 +78,7 @@
"decisions": "Entscheidungen",
"deployments": "Deployments",
"migrations": "Migrationen",
"reports": "Reports",
"admin": "Admin",
"admin-users": "Admin – Benutzer",
"admin-groups": "Admin – Gruppen",
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions public/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"deployments": "Deployments",
"batches": "Batches",
"migrations": "Migrations",
"reports": "Reports",
"admin": "Admin",
"help": "Help",
"account": "Account",
Expand Down Expand Up @@ -77,6 +78,7 @@
"decisions": "Decisions",
"deployments": "Deployments",
"migrations": "Migrations",
"reports": "Reports",
"admin": "Admin",
"admin-users": "Admin – Users",
"admin-groups": "Admin – Groups",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/api/engine_rest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -33,6 +34,7 @@ const engine_rest = {
migration,
process_definition,
process_instance,
report,
task,
tenant,
user,
Expand Down
38 changes: 38 additions & 0 deletions src/api/resources/report.js
Original file line number Diff line number Diff line change
@@ -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}` : ""}`;

Check warning on line 12 in src/api/resources/report.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not use nested template literals.

See more on https://sonarcloud.io/project/issues?id=operaton_web-apps&issues=AZ7RvMRSpEHdmJsEXRq6&open=AZ7RvMRSpEHdmJsEXRq6&pullRequest=63
};

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;
65 changes: 65 additions & 0 deletions src/api/resources/report.test.js
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
1 change: 1 addition & 0 deletions src/components/GoTo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
6 changes: 6 additions & 0 deletions src/components/GoTo.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function Header() {
<li><a href="/deployments" class={url.startsWith("/deployments") && "active"}>{t("nav.deployments")}</a></li>
<li><a href="/batches" class={url.startsWith("/batches") && "active"}>{t("nav.batches")}</a></li>
<li><a href="/migrations" class={url.startsWith("/migrations") && "active"}>{t("nav.migrations")}</a></li>
<li><a href="/reports" class={url.startsWith("/reports") && "active"}>{t("nav.reports")}</a></li>
<li><a href="/admin" class={url.startsWith("/admin") && "active"}>{t("nav.admin")}</a></li>
</menu>
</nav>
Expand Down Expand Up @@ -146,6 +147,11 @@ export function Header() {
{t("nav.migrations")}
</a>
</li>
<li>
<a href="/reports" class={url.startsWith("/reports") && "active"}>
{t("nav.reports")}
</a>
</li>
<li>
<a href="/admin" class={url.startsWith("/admin") && "active"}>
{t("nav.admin")}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe("Header", () => {
a.getAttribute("href"),
);
// Logo (/), tasks, processes, decisions, deployments, batches,
// migrations, admin.
// migrations, reports, admin.
expect(hrefs).toEqual([
"/",
"/tasks",
Expand All @@ -70,6 +70,7 @@ describe("Header", () => {
"/deployments",
"/batches",
"/migrations",
"/reports",
"/admin",
]);
});
Expand Down
51 changes: 51 additions & 0 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -90,6 +91,7 @@ const Routing = () => {
component={DeploymentsPage}
/>
<Route path="/batches/:batch_id?" component={BatchesPage} />
<Route path="/reports" component={ReportsPage} />
<Route
path="/admin/:page_id?/:selection_id?/:sub_selection_id?"
component={AdminPage}
Expand Down
Loading