From c5837c620c617888881babd425c95c1f5bf2fba0 Mon Sep 17 00:00:00 2001 From: Julian Haupt Date: Tue, 16 Jun 2026 21:49:37 +0200 Subject: [PATCH] cockpit: add instance jobs tab --- public/locales/de-DE/translation.json | 13 ++- public/locales/en-US/translation.json | 13 ++- src/api/engine_rest.jsx | 2 + src/api/resources/history.js | 8 +- src/api/resources/history.test.js | 9 ++ src/api/resources/job.js | 14 +++ src/api/resources/job.test.js | 25 +++++ src/pages/Processes.jsx | 133 +++++++++++++++++++++----- src/pages/Processes.test.jsx | 55 +++++++++++ src/state.js | 6 ++ src/state.test.js | 2 + 11 files changed, 252 insertions(+), 28 deletions(-) create mode 100644 src/api/resources/job.js create mode 100644 src/api/resources/job.test.js diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json index 5f7d6a9..96fe49a 100644 --- a/public/locales/de-DE/translation.json +++ b/public/locales/de-DE/translation.json @@ -345,7 +345,18 @@ "jobs": { "overriding-job-priority": "Überschriebene Job-Priorität", "suspend": "Anhalten", - "change-priority": "Job-Priorität ändern" + "change-priority": "Job-Priorität ändern", + "due-date": "Fälligkeitsdatum", + "timestamp": "Zeitstempel", + "retries": "Retries", + "exception-message": "Fehlermeldung", + "event": "Ereignis", + "created": "Erstellt", + "failed": "Fehlgeschlagen", + "succeeded": "Erfolgreich", + "deleted": "Gelöscht", + "log": "Log", + "empty": "Keine Jobs gefunden." }, "sort": { "name": "Name", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index ac20fbe..b515f18 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -345,7 +345,18 @@ "jobs": { "overriding-job-priority": "Overriding Job Priority", "suspend": "Suspend", - "change-priority": "Change Overriding Job Priority" + "change-priority": "Change Overriding Job Priority", + "due-date": "Due Date", + "timestamp": "Timestamp", + "retries": "Retries", + "exception-message": "Exception Message", + "event": "Event", + "created": "Created", + "failed": "Failed", + "succeeded": "Succeeded", + "deleted": "Deleted", + "log": "Log", + "empty": "No jobs found." }, "sort": { "name": "Name", diff --git a/src/api/engine_rest.jsx b/src/api/engine_rest.jsx index b1df2be..4683e38 100644 --- a/src/api/engine_rest.jsx +++ b/src/api/engine_rest.jsx @@ -12,6 +12,7 @@ import process_definition from "./resources/process_definition.js"; import process_instance from "./resources/process_instance.js"; import deployment from "./resources/deployment.js"; import history from "./resources/history.js"; +import job from "./resources/job.js"; import job_definition from "./resources/job_definition.js"; import migration from "./resources/migration.js"; import task from "./resources/task.js"; @@ -29,6 +30,7 @@ const engine_rest = { filter, group, history, + job, job_definition, migration, process_definition, diff --git a/src/api/resources/history.js b/src/api/resources/history.js index 6bc1013..50be4fc 100644 --- a/src/api/resources/history.js +++ b/src/api/resources/history.js @@ -46,6 +46,9 @@ const get_process_instance_variable = (state, instance_id) => const get_historic_tasks_by_instance = (state, instance_id) => GET(`/history/task?processInstanceId=${instance_id}`, state, state.api.history.task.by_process_instance) +const get_job_logs_by_process_instance = (state, instance_id) => + GET(`/history/job-log?processInstanceId=${instance_id}`, state, state.api.history.job_log.by_process_instance) + const get_historic_called_instances = (state, instance_id) => GET(`/history/process-instance?superProcessInstanceId=${instance_id}`, state, state.api.history.process_instance.called) @@ -72,7 +75,10 @@ const history = { task: { by_process_instance: get_historic_tasks_by_instance, }, + job_log: { + by_process_instance: get_job_logs_by_process_instance, + }, get_user_operation } -export default history \ No newline at end of file +export default history diff --git a/src/api/resources/history.test.js b/src/api/resources/history.test.js index 12970fd..07b2270 100644 --- a/src/api/resources/history.test.js +++ b/src/api/resources/history.test.js @@ -110,6 +110,15 @@ describe("api/resources/history", () => { }); }); + it("job_log.by_process_instance() GETs historic job logs filtered by instance", () => { + history.job_log.by_process_instance(state, "inst-1"); + expect_api_call(GET, { + url: "/history/job-log?processInstanceId=inst-1", + state, + signal: state.api.history.job_log.by_process_instance, + }); + }); + it("process_instance.called() GETs historic child instances of an instance", () => { history.process_instance.called(state, "inst-1"); expect_api_call(GET, { diff --git a/src/api/resources/job.js b/src/api/resources/job.js new file mode 100644 index 0000000..957580e --- /dev/null +++ b/src/api/resources/job.js @@ -0,0 +1,14 @@ +import { GET } from "../helper.jsx"; + +const get_jobs_by_process_instance = (state, process_instance_id) => + GET( + `/job?processInstanceId=${process_instance_id}`, + state, + state.api.job.by_process_instance, + ); + +const job = { + by_process_instance: get_jobs_by_process_instance, +}; + +export default job; diff --git a/src/api/resources/job.test.js b/src/api/resources/job.test.js new file mode 100644 index 0000000..d487d54 --- /dev/null +++ b/src/api/resources/job.test.js @@ -0,0 +1,25 @@ +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 job from "./job.js"; + +describe("api/resources/job", () => { + let state; + beforeEach(() => { + state = create_mock_state(); + }); + + it("by_process_instance() GETs jobs for a process instance", () => { + job.by_process_instance(state, "inst-1"); + expect_api_call(GET, { + url: "/job?processInstanceId=inst-1", + state, + signal: state.api.job.by_process_instance, + }); + }); +}); diff --git a/src/pages/Processes.jsx b/src/pages/Processes.jsx index d6ac79e..de18935 100644 --- a/src/pages/Processes.jsx +++ b/src/pages/Processes.jsx @@ -686,7 +686,7 @@ const DefinitionsEmpty = () => { {t("processes.empty.how-to")} @@ -928,8 +928,7 @@ const InstanceDetails = () => { params: { selection_id, definition_id, panel }, query, } = useRoute(), - history_mode = query.history === "true", - [t] = useTranslation(); + history_mode = query.history === "true"; if (selection_id) { if ( @@ -1043,20 +1042,16 @@ const InstanceVariables = () => { ? !history_mode ? Object.entries( state.api.process.instance.variables.value.data, - ).map( - // eslint-disable-next-line react/jsx-key - ([name, { type, value }]) => ( - - {name} - {type} - {value} - - ), - ) + ).map(([name, { type, value }]) => ( + + {name} + {type} + {value} + + )) : state.api.process.instance.variables.value.data.map( - // eslint-disable-next-line react/jsx-key ({ name, type, value }) => ( - + {name} {type} {value} @@ -1464,13 +1459,6 @@ const JobDefinitions = () => { ); }; -const BackToListBtn = ({ url, title, className }) => ( - - - - -); - const DefinitionsManage = () => { const state = useContext(AppState), { route } = useLocation(), @@ -1571,8 +1559,103 @@ const UUIDLink = ({ uuid = "?", path }) => ( ); -// TODO: create Jobs example for old Camunda apps -const InstanceJobsPlaceholder = () =>

Jobs

; +const job_log_event = (job_log) => { + if (job_log.creationLog) return "created"; + if (job_log.failureLog) return "failed"; + if (job_log.successLog) return "succeeded"; + if (job_log.deletionLog) return "deleted"; + return "log"; +}; + +const InstanceJobs = () => { + const state = useContext(AppState), + { params, query } = useRoute(), + history_mode = query.history === "true", + [t] = useTranslation(); + + useEffect(() => { + if (history_mode) { + void engine_rest.history.job_log.by_process_instance( + state, + params.selection_id, + ); + } else { + void engine_rest.job.by_process_instance(state, params.selection_id); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params.selection_id, history_mode]); + + const rows = history_mode + ? (state.api.history.job_log.by_process_instance.value?.data ?? []) + : (state.api.job.by_process_instance.value?.data ?? []); + + return ( +
+ + + + + + + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((job) => { + const id = history_mode ? (job.jobId ?? job.id) : job.id, + state_or_event = history_mode + ? t(`processes.jobs.${job_log_event(job)}`) + : job.suspended + ? t("common.suspended") + : t("common.active"), + activity = job.activityId ?? job.failedActivityId ?? "—", + due_date = history_mode + ? (job.timestamp ?? job.jobDueDate) + : job.dueDate, + retries = history_mode ? job.jobRetries : job.retries, + priority = history_mode ? job.jobPriority : job.priority, + exception_message = history_mode + ? job.jobExceptionMessage + : job.exceptionMessage; + + return ( + + + + + + + + + + ); + }) + )} + +
{t("common.id")} + {history_mode ? t("processes.jobs.event") : t("common.state")} + {t("common.activity")} + {history_mode + ? t("processes.jobs.timestamp") + : t("processes.jobs.due-date")} + {t("processes.jobs.retries")}{t("tasks.task-list.table-headings.priority")}{t("processes.jobs.exception-message")}
{t("processes.jobs.empty")}
{id?.substring(0, 8) ?? "—"}{state_or_event}{activity} + {due_date ? ( + + ) : ( + "—" + )} + {retries ?? "—"}{priority ?? "—"}{exception_message ?? "—"}
+
+ ); +}; // TODO: create External Apps example for old Camunda apps const InstanceExternalTasksPlaceholder = () =>

External Tasks

; @@ -1605,7 +1688,7 @@ const process_instance_tabs = [ nameKey: "processes.tabs.jobs", id: "jobs", pos: 4, - Component: InstanceJobsPlaceholder, + Component: InstanceJobs, }, { nameKey: "processes.tabs.external-tasks", diff --git a/src/pages/Processes.test.jsx b/src/pages/Processes.test.jsx index 60c4b48..373967d 100644 --- a/src/pages/Processes.test.jsx +++ b/src/pages/Processes.test.jsx @@ -506,6 +506,61 @@ describe("ProcessesPage — instance details", () => { expect(getByText("Approve (historic)")).toBeTruthy(); }); + it("jobs sub-panel fetches and renders live jobs", () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "jobs", + }; + signal_response(state.api.job.by_process_instance, [ + { + id: "job-1", + activityId: "timerEvent", + dueDate: "2024-01-01T00:00:00Z", + retries: 3, + priority: 50, + suspended: false, + exceptionMessage: "live boom", + }, + ]); + const { getByText } = renderPage(state); + expect(engine_rest.job.by_process_instance).toHaveBeenCalled(); + expect(getByText("job-1")).toBeTruthy(); + expect(getByText("timerEvent")).toBeTruthy(); + expect(getByText("live boom")).toBeTruthy(); + }); + + it("jobs sub-panel fetches and renders historic job logs in history mode", () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "jobs", + }; + mockQuery = { history: "true" }; + signal_response(state.api.history.job_log.by_process_instance, [ + { + id: "job-log-1", + jobId: "job-77", + activityId: "serviceTask", + timestamp: "2024-01-01T00:00:00Z", + jobRetries: 0, + jobPriority: 80, + jobExceptionMessage: "history boom", + failureLog: true, + }, + ]); + const { getByText } = renderPage(state); + expect(engine_rest.history.job_log.by_process_instance).toHaveBeenCalled(); + expect(engine_rest.job.by_process_instance).not.toHaveBeenCalled(); + expect(getByText("job-77")).toBeTruthy(); + expect(getByText("serviceTask")).toBeTruthy(); + expect(getByText("2024-01-01T00:00:00Z")).toBeTruthy(); + expect(getByText("processes.jobs.failed")).toBeTruthy(); + expect(getByText("history boom")).toBeTruthy(); + }); + it("called-instances sub-panel uses the historic endpoint in history mode", () => { mockParams = { definition_id: "proc:1", diff --git a/src/state.js b/src/state.js index f63ad2e..ed3d2a3 100644 --- a/src/state.js +++ b/src/state.js @@ -145,6 +145,9 @@ const createAppState = () => { create: signal(null), }, }, + job: { + by_process_instance: signal(null), + }, filter: { list: signal(null), one: signal(null), @@ -174,6 +177,9 @@ const createAppState = () => { task: { by_process_instance: signal(null), }, + job_log: { + by_process_instance: signal(null), + }, process_instance: { called: signal(null), }, diff --git a/src/state.test.js b/src/state.test.js index c6b9968..3fb64e6 100644 --- a/src/state.test.js +++ b/src/state.test.js @@ -53,6 +53,8 @@ describe("state", () => { const { api } = createAppState(); expect(api.process.definition.list.value).toBeNull(); expect(api.process.instance.one.value).toBeNull(); + expect(api.job.by_process_instance.value).toBeNull(); + expect(api.history.job_log.by_process_instance.value).toBeNull(); expect(api.task.comment.list.value).toBeNull(); expect(api.authorization.all.value).toBeNull(); expect(api.batch.list.value).toBeNull();