{t("cleanup.title")}
+{t("cleanup.subtitle")}
+diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json index 5f7d6a9..806d99a 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", + "cleanup": "Cleanup", "admin": "Admin", "help": "Hilfe", "account": "Konto", @@ -77,6 +78,7 @@ "decisions": "Entscheidungen", "deployments": "Deployments", "migrations": "Migrationen", + "cleanup": "Cleanup", "admin": "Admin", "admin-users": "Admin – Benutzer", "admin-groups": "Admin – Gruppen", @@ -559,6 +561,37 @@ "without-tenant-id": "Ohne Mandanten-ID" } }, + "cleanup": { + "title": "Cleanup", + "subtitle": "History Cleanup überwachen, bereinigbare Daten prüfen und die History Time To Live anpassen.", + "refresh": "Aktualisieren", + "run-now": "Cleanup jetzt starten", + "configuration": "Konfiguration", + "enabled": "Aktiviert", + "strategy": "Strategie", + "batch-window": "Batch-Fenster", + "parallelism": "Parallelität", + "deleted-data": "Gelöschte Daten in den letzten {{days}} Tagen", + "jobs": "Cleanup-Jobs", + "no-jobs": "Es sind keine Cleanup-Jobs geplant.", + "due-date": "Fälligkeitsdatum", + "retries": "Retries", + "exception": "Exception", + "process-definitions": "Bereinigbare Prozessdefinitionen", + "decision-definitions": "Bereinigbare Entscheidungsdefinitionen", + "batches": "Bereinigbare Batches", + "no-cleanable-data": "Keine bereinigbaren Daten gemeldet.", + "version": "Version", + "finished": "Beendet", + "cleanable": "Bereinigbar", + "ttl": "History TTL", + "save-ttl": "TTL speichern", + "metrics": { + "process-instances": "Prozessinstanzen", + "decision-instances": "Entscheidungsinstanzen", + "batch-operations": "Batch-Operationen" + } + }, "admin": { "users": "Benutzer", "groups": "Gruppen", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index ac20fbe..7c63e59 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", + "cleanup": "Cleanup", "admin": "Admin", "help": "Help", "account": "Account", @@ -77,6 +78,7 @@ "decisions": "Decisions", "deployments": "Deployments", "migrations": "Migrations", + "cleanup": "Cleanup", "admin": "Admin", "admin-users": "Admin – Users", "admin-groups": "Admin – Groups", @@ -559,6 +561,37 @@ "without-tenant-id": "Without Tenant ID" } }, + "cleanup": { + "title": "Cleanup", + "subtitle": "Monitor history cleanup, review cleanable data, and adjust history time to live.", + "refresh": "Refresh", + "run-now": "Run cleanup now", + "configuration": "Configuration", + "enabled": "Enabled", + "strategy": "Strategy", + "batch-window": "Batch window", + "parallelism": "Parallelism", + "deleted-data": "Deleted data in the last {{days}} days", + "jobs": "Cleanup jobs", + "no-jobs": "No cleanup jobs are scheduled.", + "due-date": "Due date", + "retries": "Retries", + "exception": "Exception", + "process-definitions": "Cleanable process definitions", + "decision-definitions": "Cleanable decision definitions", + "batches": "Cleanable batches", + "no-cleanable-data": "No cleanable data reported.", + "version": "Version", + "finished": "Finished", + "cleanable": "Cleanable", + "ttl": "History TTL", + "save-ttl": "Save TTL", + "metrics": { + "process-instances": "Process instances", + "decision-instances": "Decision instances", + "batch-operations": "Batch operations" + } + }, "admin": { "users": "Users", "groups": "Groups", diff --git a/src/api/engine_rest.jsx b/src/api/engine_rest.jsx index b1df2be..6887238 100644 --- a/src/api/engine_rest.jsx +++ b/src/api/engine_rest.jsx @@ -4,6 +4,7 @@ import { } from "./helper.jsx"; import auth from "./resources/auth.js"; import batch from "./resources/batch.js"; +import cleanup from "./resources/cleanup.js"; import engine from "./resources/engine.js"; import user from "./resources/user.js"; import group from "./resources/group.js"; @@ -23,6 +24,7 @@ const engine_rest = { auth, authorization, batch, + cleanup, decision, deployment, engine, diff --git a/src/api/resources/cleanup.js b/src/api/resources/cleanup.js new file mode 100644 index 0000000..a776a9c --- /dev/null +++ b/src/api/resources/cleanup.js @@ -0,0 +1,98 @@ +import { GET, POST, PUT } from "../helper.jsx"; + +const metric_url = (name, params = {}) => { + const query = new URLSearchParams(params).toString(); + return `/metrics/${name}/sum${query ? `?${query}` : ""}`; +}; + +const get_configuration = (state) => + GET("/history/cleanup/configuration", state, state.api.cleanup.configuration); + +const get_jobs = (state) => + GET("/history/cleanup/jobs", state, state.api.cleanup.jobs); + +const run_cleanup = (state, immediately_due = false) => + POST( + `/history/cleanup?immediatelyDue=${immediately_due}`, + {}, + state, + state.api.cleanup.run, + ); + +const get_cleanable_process_definitions = (state) => + GET( + "/history/process-definition/cleanable-process-instance-report", + state, + state.api.cleanup.cleanable.process_definitions, + ); + +const get_cleanable_decision_definitions = (state) => + GET( + "/history/decision-definition/cleanable-decision-instance-report", + state, + state.api.cleanup.cleanable.decision_definitions, + ); + +const get_cleanable_batches = (state) => + GET( + "/history/batch/cleanable-batch-report", + state, + state.api.cleanup.cleanable.batches, + ); + +const get_removed_process_instances = (state, params = {}) => + GET( + metric_url("history-cleanup-removed-process-instances", params), + state, + state.api.cleanup.metrics.process_instances, + ); + +const get_removed_decision_instances = (state, params = {}) => + GET( + metric_url("history-cleanup-removed-decision-instances", params), + state, + state.api.cleanup.metrics.decision_instances, + ); + +const get_removed_batch_operations = (state, params = {}) => + GET( + metric_url("history-cleanup-removed-batch-operations", params), + state, + state.api.cleanup.metrics.batch_operations, + ); + +const set_process_definition_ttl = (state, id, historyTimeToLive) => + PUT( + `/process-definition/${id}/history-time-to-live`, + { historyTimeToLive }, + state, + state.api.cleanup.update_ttl, + ); + +const set_decision_definition_ttl = (state, id, historyTimeToLive) => + PUT( + `/decision-definition/${id}/history-time-to-live`, + { historyTimeToLive }, + state, + state.api.cleanup.update_ttl, + ); + +const cleanup = { + configuration: get_configuration, + jobs: get_jobs, + run: run_cleanup, + cleanable: { + process_definitions: get_cleanable_process_definitions, + decision_definitions: get_cleanable_decision_definitions, + batches: get_cleanable_batches, + }, + metrics: { + process_instances: get_removed_process_instances, + decision_instances: get_removed_decision_instances, + batch_operations: get_removed_batch_operations, + }, + set_process_definition_ttl, + set_decision_definition_ttl, +}; + +export default cleanup; diff --git a/src/api/resources/cleanup.test.js b/src/api/resources/cleanup.test.js new file mode 100644 index 0000000..e18e9dd --- /dev/null +++ b/src/api/resources/cleanup.test.js @@ -0,0 +1,124 @@ +import { describe, it, vi, beforeEach } from "vitest"; + +vi.mock("../helper.jsx", () => ({ + GET: vi.fn(), + POST: vi.fn(), + PUT: vi.fn(), +})); + +import { GET, POST, PUT } from "../helper.jsx"; +import { create_mock_state, expect_api_call } from "../../test/helpers.js"; +import cleanup from "./cleanup.js"; + +describe("api/resources/cleanup", () => { + let state; + + beforeEach(() => { + state = create_mock_state(); + }); + + it("configuration() GETs history cleanup configuration", () => { + cleanup.configuration(state); + expect_api_call(GET, { + url: "/history/cleanup/configuration", + state, + signal: state.api.cleanup.configuration, + }); + }); + + it("jobs() GETs history cleanup jobs", () => { + cleanup.jobs(state); + expect_api_call(GET, { + url: "/history/cleanup/jobs", + state, + signal: state.api.cleanup.jobs, + }); + }); + + it("run() POSTs the cleanup trigger", () => { + cleanup.run(state, true); + expect_api_call(POST, { + url: "/history/cleanup?immediatelyDue=true", + body: {}, + state, + signal: state.api.cleanup.run, + }); + }); + + it("cleanable.process_definitions() GETs the cleanable process report", () => { + cleanup.cleanable.process_definitions(state); + expect_api_call(GET, { + url: "/history/process-definition/cleanable-process-instance-report", + state, + signal: state.api.cleanup.cleanable.process_definitions, + }); + }); + + it("cleanable.decision_definitions() GETs the cleanable decision report", () => { + cleanup.cleanable.decision_definitions(state); + expect_api_call(GET, { + url: "/history/decision-definition/cleanable-decision-instance-report", + state, + signal: state.api.cleanup.cleanable.decision_definitions, + }); + }); + + it("cleanable.batches() GETs the cleanable batch report", () => { + cleanup.cleanable.batches(state); + expect_api_call(GET, { + url: "/history/batch/cleanable-batch-report", + state, + signal: state.api.cleanup.cleanable.batches, + }); + }); + + it("metrics.process_instances() GETs the metric sum", () => { + cleanup.metrics.process_instances(state, { + startDate: "2026-05-17T00:00:00.000+0000", + endDate: "2026-06-16T00:00:00.000+0000", + }); + expect_api_call(GET, { + url: "/metrics/history-cleanup-removed-process-instances/sum?startDate=2026-05-17T00%3A00%3A00.000%2B0000&endDate=2026-06-16T00%3A00%3A00.000%2B0000", + state, + signal: state.api.cleanup.metrics.process_instances, + }); + }); + + it("metrics.decision_instances() GETs the metric sum", () => { + cleanup.metrics.decision_instances(state); + expect_api_call(GET, { + url: "/metrics/history-cleanup-removed-decision-instances/sum", + state, + signal: state.api.cleanup.metrics.decision_instances, + }); + }); + + it("metrics.batch_operations() GETs the metric sum", () => { + cleanup.metrics.batch_operations(state); + expect_api_call(GET, { + url: "/metrics/history-cleanup-removed-batch-operations/sum", + state, + signal: state.api.cleanup.metrics.batch_operations, + }); + }); + + it("set_process_definition_ttl() PUTs historyTimeToLive", () => { + cleanup.set_process_definition_ttl(state, "process:1", 30); + expect_api_call(PUT, { + url: "/process-definition/process:1/history-time-to-live", + body: { historyTimeToLive: 30 }, + state, + signal: state.api.cleanup.update_ttl, + }); + }); + + it("set_decision_definition_ttl() PUTs nullable historyTimeToLive", () => { + cleanup.set_decision_definition_ttl(state, "decision:1", null); + expect_api_call(PUT, { + url: "/decision-definition/decision:1/history-time-to-live", + body: { historyTimeToLive: null }, + state, + signal: state.api.cleanup.update_ttl, + }); + }); +}); diff --git a/src/components/GoTo.jsx b/src/components/GoTo.jsx index 87dde84..9f158c0 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.cleanup", href: "/cleanup" }, { 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..460e5f3 100644 --- a/src/components/GoTo.test.jsx +++ b/src/components/GoTo.test.jsx @@ -53,6 +53,13 @@ describe("GoTo", () => { a.getAttribute("href"), ), ).not.toContain("/tasks"); + + type(input, "cleanup"); + expect( + Array.from(container.querySelectorAll(".goto-item")).map((a) => + a.getAttribute("href"), + ), + ).toContain("/cleanup"); }); it("navigates and closes when a result is clicked", () => { diff --git a/src/components/Header.jsx b/src/components/Header.jsx index ce108ea..e378532 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -74,6 +74,7 @@ export function Header() { @@ -146,6 +147,11 @@ export function Header() { {t("nav.migrations")} +
{t("cleanup.subtitle")}
+{t("cleanup.no-jobs")}
; + return ( +| {t("common.id")} | +{t("cleanup.due-date")} | +{t("cleanup.retries")} | +{t("cleanup.exception")} | +
|---|---|---|---|
| {job.id} | +{nullable(job.dueDate)} | +{nullable(job.retries)} | +{nullable(job.exceptionMessage)} | +
{t("cleanup.no-cleanable-data")}
; + return ( +| {t("common.name")} | +{t("common.key")} | +{t("cleanup.version")} | +{t("cleanup.finished")} | +{t("cleanup.cleanable")} | +{t("cleanup.ttl")} | +
|---|---|---|---|---|---|
| + + {row.processDefinitionName ?? + row.processDefinitionKey ?? + row.processDefinitionId} + + | +{row.processDefinitionKey} | +{row.processDefinitionVersion} | +{num(row.finishedProcessInstanceCount)} | +{num(row.cleanableProcessInstanceCount)} | +
+ |
+
{t("cleanup.no-cleanable-data")}
; + return ( +| {t("common.name")} | +{t("common.key")} | +{t("cleanup.version")} | +{t("cleanup.finished")} | +{t("cleanup.cleanable")} | +{t("cleanup.ttl")} | +
|---|---|---|---|---|---|
| + + {row.decisionDefinitionName ?? + row.decisionDefinitionKey ?? + row.decisionDefinitionId} + + | +{row.decisionDefinitionKey} | +{row.decisionDefinitionVersion} | +{num(row.finishedDecisionInstanceCount)} | +{num(row.cleanableDecisionInstanceCount)} | +
+ |
+
{t("cleanup.no-cleanable-data")}
; + return ( +| {t("common.type")} | +{t("cleanup.finished")} | +{t("cleanup.cleanable")} | +{t("cleanup.ttl")} | +
|---|---|---|---|
| {row.batchType} | +{num(row.finishedBatchCount)} | +{num(row.cleanableBatchCount)} | +{nullable(row.historyTimeToLive)} | +