diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json index 5f7d6a9..a072fe5 100644 --- a/public/locales/de-DE/translation.json +++ b/public/locales/de-DE/translation.json @@ -370,6 +370,9 @@ "startableInTasklist": "startableInTasklist" }, "instance": { + "activate": "Instanz aktivieren", + "suspend": "Instanz anhalten", + "suspension-success": "Prozessinstanz-Status aktualisiert.", "sort": { "startTime": "Startzeit", "instanceId": "Instanz-ID", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index ac20fbe..32c133e 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -370,6 +370,9 @@ "startableInTasklist": "startableInTasklist" }, "instance": { + "activate": "Activate Instance", + "suspend": "Suspend Instance", + "suspension-success": "Process instance state updated.", "sort": { "startTime": "Start time", "instanceId": "Instance ID", diff --git a/src/api/resources/process_instance.js b/src/api/resources/process_instance.js index d1d6c02..86b0136 100644 --- a/src/api/resources/process_instance.js +++ b/src/api/resources/process_instance.js @@ -1,4 +1,4 @@ -import { GET, POST } from "../helper.jsx"; +import { GET, POST, PUT } from "../helper.jsx"; const get_process_instance = (state, instance_id) => GET( @@ -33,6 +33,14 @@ const get_process_instance_variables = (state, instance_id) => state.api.process.instance.variables, ); +const set_process_instance_suspension_state = (state, instance_id, suspended) => + PUT( + `/process-instance/${instance_id}/suspended`, + { suspended }, + state, + state.api.process.instance.suspension, + ); + const get_called_process_instances = (state, instance_id) => GET( `/process-instance?superProcessInstance=${instance_id}`, @@ -105,6 +113,10 @@ const modify_process_instance_async = ( const process_instance = { one: get_process_instance, variables: get_process_instance_variables, + activate: (state, instance_id) => + set_process_instance_suspension_state(state, instance_id, false), + suspend: (state, instance_id) => + set_process_instance_suspension_state(state, instance_id, true), called: get_called_process_instances, count: get_process_instance_count, all: get_all_process_instances, diff --git a/src/api/resources/process_instance.test.js b/src/api/resources/process_instance.test.js index 90bb054..ff4d231 100644 --- a/src/api/resources/process_instance.test.js +++ b/src/api/resources/process_instance.test.js @@ -3,9 +3,10 @@ import { describe, it, vi, beforeEach } from "vitest"; vi.mock("../helper.jsx", () => ({ GET: vi.fn(), POST: vi.fn(), + PUT: vi.fn(), })); -import { GET, POST } from "../helper.jsx"; +import { GET, POST, PUT } from "../helper.jsx"; import { create_mock_state, expect_api_call } from "../../test/helpers.js"; import process_instance from "./process_instance.js"; @@ -33,6 +34,26 @@ describe("api/resources/process_instance", () => { }); }); + it("suspend() PUTs a suspended process instance state", () => { + process_instance.suspend(state, "inst-1"); + expect_api_call(PUT, { + url: "/process-instance/inst-1/suspended", + body: { suspended: true }, + state, + signal: state.api.process.instance.suspension, + }); + }); + + it("activate() PUTs an active process instance state", () => { + process_instance.activate(state, "inst-1"); + expect_api_call(PUT, { + url: "/process-instance/inst-1/suspended", + body: { suspended: false }, + state, + signal: state.api.process.instance.suspension, + }); + }); + it("called() GETs sub process instances by superProcessInstance", () => { process_instance.called(state, "inst-1"); expect_api_call(GET, { diff --git a/src/pages/Processes.jsx b/src/pages/Processes.jsx index d6ac79e..38be567 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 ( @@ -965,21 +964,67 @@ const InstanceDetails = () => { const InstanceDetailsDescription = () => { const state = useContext(AppState), [t] = useTranslation(), + { query } = useRoute(), + history_mode = query.history === "true", data = state.api.process.instance.one.value?.data; + const set_suspension_state = (suspended) => { + if (!data?.id) return; + const action = suspended ? "suspend" : "activate"; + void Promise.resolve(engine_rest.process_instance[action](state, data.id)).then( + () => engine_rest.process_instance.one(state, data.id), + ); + }; return ( -
-
{t("processes.instance-id")}
-
- {data?.id ?? "—"} -
-
{t("processes.business-key")}
-
{data?.businessKey ?? "—"}
-
+ <> + null} + on_success={() => ( +

{t("processes.instance.suspension-success")}

+ )} + /> +
+
{t("processes.instance-id")}
+
+ {data?.id ?? "—"} +
+
{t("processes.business-key")}
+
{data?.businessKey ?? "—"}
+
{t("common.state")}
+
+ {data?.suspended === true + ? t("common.suspended") + : data?.suspended === false + ? t("common.active") + : "—"} +
+
+ {!history_mode && data?.id ? ( +
+ {data.suspended ? ( + + ) : ( + + )} +
+ ) : null} + ); }; @@ -1043,20 +1088,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 +1505,6 @@ const JobDefinitions = () => { ); }; -const BackToListBtn = ({ url, title, className }) => ( - - - - -); - const DefinitionsManage = () => { const state = useContext(AppState), { route } = useLocation(), diff --git a/src/pages/Processes.test.jsx b/src/pages/Processes.test.jsx index 60c4b48..0222d7f 100644 --- a/src/pages/Processes.test.jsx +++ b/src/pages/Processes.test.jsx @@ -389,6 +389,79 @@ describe("ProcessesPage — instance details", () => { expect(getByText("BK-9")).toBeTruthy(); }); + it("suspends a live process instance", async () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "vars", + }; + signal_response(state.api.process.instance.one, { + id: "inst-9999", + businessKey: "BK-9", + suspended: false, + }); + const { getByText } = renderPage(state); + + fireEvent.click(getByText("processes.instance.suspend")); + await Promise.resolve(); + await Promise.resolve(); + + expect(engine_rest.process_instance.suspend).toHaveBeenCalled(); + expect(engine_rest.process_instance.suspend.mock.lastCall[0]).toBe(state); + expect(engine_rest.process_instance.suspend.mock.lastCall[1]).toBe( + "inst-9999", + ); + expect(engine_rest.process_instance.one.mock.lastCall[0]).toBe(state); + expect(engine_rest.process_instance.one.mock.lastCall[1]).toBe("inst-9999"); + }); + + it("activates a suspended live process instance", async () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "vars", + }; + signal_response(state.api.process.instance.one, { + id: "inst-9999", + businessKey: "BK-9", + suspended: true, + }); + const { getByText } = renderPage(state); + + fireEvent.click(getByText("processes.instance.activate")); + await Promise.resolve(); + await Promise.resolve(); + + expect(engine_rest.process_instance.activate).toHaveBeenCalled(); + expect(engine_rest.process_instance.activate.mock.lastCall[0]).toBe(state); + expect(engine_rest.process_instance.activate.mock.lastCall[1]).toBe( + "inst-9999", + ); + expect(engine_rest.process_instance.one.mock.lastCall[0]).toBe(state); + expect(engine_rest.process_instance.one.mock.lastCall[1]).toBe("inst-9999"); + }); + + it("keeps historic process instance details read-only", () => { + mockParams = { + definition_id: "proc:1", + panel: "instances", + selection_id: "inst-9999", + sub_panel: "vars", + }; + mockQuery = { history: "true" }; + signal_response(state.api.process.instance.one, { + id: "inst-9999", + businessKey: "BK-9", + suspended: false, + }); + const { queryByText } = renderPage(state); + + expect(queryByText("processes.instance.suspend")).toBeNull(); + expect(queryByText("processes.instance.activate")).toBeNull(); + }); + it("variables sub-panel fetches and renders live variables", () => { mockParams = { definition_id: "proc:1", diff --git a/src/state.js b/src/state.js index f63ad2e..820797f 100644 --- a/src/state.js +++ b/src/state.js @@ -119,6 +119,7 @@ const createAppState = () => { list: signal(null), count: signal(null), variables: signal(null), + suspension: signal(null), by_defintion_id: signal(null), activity_instances: signal(null), modification: signal(null),