diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json index 5f7d6a9..1bf6248 100644 --- a/public/locales/de-DE/translation.json +++ b/public/locales/de-DE/translation.json @@ -342,6 +342,14 @@ "called-definitions": { "called-process-definition": "Aufgerufene Prozessdefinition" }, + "external-tasks": { + "topic": "Thema", + "worker": "Worker", + "lock-expiration": "Sperrablauf", + "retries": "Retries", + "error-message": "Fehlermeldung", + "empty": "Keine externen Aufgaben gefunden." + }, "jobs": { "overriding-job-priority": "Überschriebene Job-Priorität", "suspend": "Anhalten", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index ac20fbe..80ad974 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -342,6 +342,14 @@ "called-definitions": { "called-process-definition": "Called Process Definition" }, + "external-tasks": { + "topic": "Topic", + "worker": "Worker", + "lock-expiration": "Lock Expiration", + "retries": "Retries", + "error-message": "Error Message", + "empty": "No external tasks found." + }, "jobs": { "overriding-job-priority": "Overriding Job Priority", "suspend": "Suspend", diff --git a/src/api/engine_rest.jsx b/src/api/engine_rest.jsx index b1df2be..f482391 100644 --- a/src/api/engine_rest.jsx +++ b/src/api/engine_rest.jsx @@ -11,6 +11,7 @@ import tenant from "./resources/tenant.js"; import process_definition from "./resources/process_definition.js"; import process_instance from "./resources/process_instance.js"; import deployment from "./resources/deployment.js"; +import external_task from "./resources/external_task.js"; import history from "./resources/history.js"; import job_definition from "./resources/job_definition.js"; import migration from "./resources/migration.js"; @@ -26,6 +27,7 @@ const engine_rest = { decision, deployment, engine, + external_task, filter, group, history, diff --git a/src/api/resources/external_task.js b/src/api/resources/external_task.js new file mode 100644 index 0000000..94d3ff0 --- /dev/null +++ b/src/api/resources/external_task.js @@ -0,0 +1,14 @@ +import { GET } from "../helper.jsx"; + +const get_external_tasks_by_process_instance = (state, process_instance_id) => + GET( + `/external-task?processInstanceId=${process_instance_id}`, + state, + state.api.external_task.by_process_instance, + ); + +const external_task = { + by_process_instance: get_external_tasks_by_process_instance, +}; + +export default external_task; diff --git a/src/api/resources/external_task.test.js b/src/api/resources/external_task.test.js new file mode 100644 index 0000000..4033087 --- /dev/null +++ b/src/api/resources/external_task.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 external_task from "./external_task.js"; + +describe("api/resources/external_task", () => { + let state; + beforeEach(() => { + state = create_mock_state(); + }); + + it("by_process_instance() GETs external tasks for a process instance", () => { + external_task.by_process_instance(state, "inst-1"); + expect_api_call(GET, { + url: "/external-task?processInstanceId=inst-1", + state, + signal: state.api.external_task.by_process_instance, + }); + }); +}); diff --git a/src/pages/Processes.jsx b/src/pages/Processes.jsx index d6ac79e..9508630 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(), @@ -1573,8 +1561,75 @@ const UUIDLink = ({ uuid = "?", path }) => ( // TODO: create Jobs example for old Camunda apps const InstanceJobsPlaceholder = () =>

Jobs

; -// TODO: create External Apps example for old Camunda apps -const InstanceExternalTasksPlaceholder = () =>

External Tasks

; +const InstanceExternalTasks = () => { + const state = useContext(AppState), + { selection_id, query } = useRoute(), + history_mode = query.history === "true", + [t] = useTranslation(); + + useEffect(() => { + if (!history_mode) { + void engine_rest.external_task.by_process_instance(state, selection_id); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selection_id, history_mode]); + + if (history_mode) { + return

{t("processes.history-mode-na")}

; + } + + /** @namespace state.api.external_task.by_process_instance.value.data **/ + return ( + { + const rows = + state.api.external_task.by_process_instance.value?.data ?? []; + if (rows.length === 0) { + return

{t("processes.external-tasks.empty")}

; + } + return ( + + + + + + + + + + + + + + + {rows.map((task) => ( + + + + + + + + + + + ))} + +
{t("common.id")}{t("common.activity")}{t("processes.external-tasks.topic")}{t("processes.external-tasks.worker")}{t("processes.external-tasks.lock-expiration")}{t("processes.external-tasks.retries")}{t("tasks.task-list.table-headings.priority")}{t("processes.external-tasks.error-message")}
{task.id?.substring(0, 8)}{task.activityId ?? "—"}{task.topicName ?? "—"}{task.workerId ?? "—"} + {task.lockExpirationTime ? ( + + ) : ( + "—" + )} + {task.retries ?? "—"}{task.priority ?? "—"}{task.errorMessage ?? "—"}
+ ); + }} + /> + ); +}; const process_instance_tabs = [ { @@ -1611,7 +1666,7 @@ const process_instance_tabs = [ nameKey: "processes.tabs.external-tasks", id: "external_tasks", pos: 5, - Component: InstanceExternalTasksPlaceholder, + Component: InstanceExternalTasks, }, ]; diff --git a/src/pages/Processes.test.jsx b/src/pages/Processes.test.jsx index 60c4b48..a970d3f 100644 --- a/src/pages/Processes.test.jsx +++ b/src/pages/Processes.test.jsx @@ -506,6 +506,47 @@ describe("ProcessesPage — instance details", () => { expect(getByText("Approve (historic)")).toBeTruthy(); }); + it("external-tasks sub-panel fetches and renders external tasks", () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "external_tasks", + }; + signal_response(state.api.external_task.by_process_instance, [ + { + id: "external-task-1", + activityId: "serviceTask", + topicName: "billing-topic", + workerId: "worker-1", + lockExpirationTime: "2024-01-01T00:00:00Z", + retries: 3, + priority: 50, + errorMessage: "boom", + }, + ]); + const { getByText } = renderPage(state); + expect(engine_rest.external_task.by_process_instance).toHaveBeenCalled(); + expect(getByText("external")).toBeTruthy(); + expect(getByText("serviceTask")).toBeTruthy(); + expect(getByText("billing-topic")).toBeTruthy(); + expect(getByText("worker-1")).toBeTruthy(); + expect(getByText("boom")).toBeTruthy(); + }); + + it("external-tasks sub-panel does not fetch live external tasks in history mode", () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "external_tasks", + }; + mockQuery = { history: "true" }; + const { getByText } = renderPage(state); + expect(engine_rest.external_task.by_process_instance).not.toHaveBeenCalled(); + expect(getByText("processes.history-mode-na")).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..a945ffa 100644 --- a/src/state.js +++ b/src/state.js @@ -160,6 +160,9 @@ const createAppState = () => { delete: signal(null), saved_filters: signal(null), }, + external_task: { + by_process_instance: signal(null), + }, decision: { definitions: signal(null), definition: signal(null), diff --git a/src/state.test.js b/src/state.test.js index c6b9968..994ad04 100644 --- a/src/state.test.js +++ b/src/state.test.js @@ -53,6 +53,7 @@ describe("state", () => { const { api } = createAppState(); expect(api.process.definition.list.value).toBeNull(); expect(api.process.instance.one.value).toBeNull(); + expect(api.external_task.by_process_instance.value).toBeNull(); expect(api.task.comment.list.value).toBeNull(); expect(api.authorization.all.value).toBeNull(); expect(api.batch.list.value).toBeNull();