diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json index 5f7d6a9..99e9b9e 100644 --- a/public/locales/de-DE/translation.json +++ b/public/locales/de-DE/translation.json @@ -324,7 +324,10 @@ "cause-process-instance-id": "Verursachende Prozessinstanz-ID", "root-cause-process-instance-id": "Ursprüngliche Prozessinstanz-ID", "annotation": "Anmerkung", - "configuration": "Konfiguration" + "configuration": "Konfiguration", + "annotation-placeholder": "Anmerkung hinzufügen", + "save-annotation": "Speichern", + "clear-annotation": "Leeren" }, "user-tasks": { "owner": "Besitzer", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index ac20fbe..e50e46f 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -324,7 +324,10 @@ "cause-process-instance-id": "Cause Process Instance ID", "root-cause-process-instance-id": "Root Cause Process Instance ID", "annotation": "Annotation", - "configuration": "Configuration" + "configuration": "Configuration", + "annotation-placeholder": "Add annotation", + "save-annotation": "Save", + "clear-annotation": "Clear" }, "user-tasks": { "owner": "Owner", diff --git a/public/locales/es-ES/translation.json b/public/locales/es-ES/translation.json index 121792c..41691cf 100644 --- a/public/locales/es-ES/translation.json +++ b/public/locales/es-ES/translation.json @@ -291,7 +291,10 @@ "cause-process-instance-id": "ID de instancia de proceso causa", "root-cause-process-instance-id": "ID de instancia de proceso causa raíz", "annotation": "Anotación", - "configuration": "Configuración" + "configuration": "Configuración", + "annotation-placeholder": "Añadir anotación", + "save-annotation": "Guardar", + "clear-annotation": "Borrar" }, "user-tasks": { "owner": "Propietario", diff --git a/public/locales/fr-FR/translation.json b/public/locales/fr-FR/translation.json index 43e715f..44ffd23 100644 --- a/public/locales/fr-FR/translation.json +++ b/public/locales/fr-FR/translation.json @@ -291,7 +291,10 @@ "cause-process-instance-id": "ID d'instance de processus cause", "root-cause-process-instance-id": "ID d'instance de processus cause racine", "annotation": "Annotation", - "configuration": "Configuration" + "configuration": "Configuration", + "annotation-placeholder": "Ajouter une annotation", + "save-annotation": "Enregistrer", + "clear-annotation": "Effacer" }, "user-tasks": { "owner": "Propriétaire", diff --git a/public/locales/nl-NL/translation.json b/public/locales/nl-NL/translation.json index a27f3a1..7f50375 100644 --- a/public/locales/nl-NL/translation.json +++ b/public/locales/nl-NL/translation.json @@ -291,7 +291,10 @@ "cause-process-instance-id": "Oorzaak procesinstantie-ID", "root-cause-process-instance-id": "Hoofdoorzaak procesinstantie-ID", "annotation": "Annotatie", - "configuration": "Configuratie" + "configuration": "Configuratie", + "annotation-placeholder": "Annotatie toevoegen", + "save-annotation": "Opslaan", + "clear-annotation": "Wissen" }, "user-tasks": { "owner": "Eigenaar", diff --git a/src/api/engine_rest.jsx b/src/api/engine_rest.jsx index b1df2be..ed97d71 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 incident from "./resources/incident.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, + incident, job_definition, migration, process_definition, diff --git a/src/api/resources/incident.js b/src/api/resources/incident.js new file mode 100644 index 0000000..854a114 --- /dev/null +++ b/src/api/resources/incident.js @@ -0,0 +1,40 @@ +import { DELETE, GET, PUT } from "../helper.jsx"; + +const get_incidents_by_process_definition = (state, definition_id) => + GET( + `/incident?processDefinitionId=${definition_id}`, + state, + state.api.incident.by_process_definition, + ); + +const get_incidents_by_process_instance = (state, instance_id) => + GET( + `/incident?processInstanceId=${instance_id}`, + state, + state.api.incident.by_process_instance, + ); + +const set_annotation = (state, id, annotation) => + PUT( + `/incident/${id}/annotation`, + { annotation }, + state, + state.api.incident.annotation, + ); + +const clear_annotation = (state, id) => + DELETE( + `/incident/${id}/annotation`, + null, + state, + state.api.incident.annotation, + ); + +const incident = { + by_process_definition: get_incidents_by_process_definition, + by_process_instance: get_incidents_by_process_instance, + set_annotation, + clear_annotation, +}; + +export default incident; diff --git a/src/api/resources/incident.test.js b/src/api/resources/incident.test.js new file mode 100644 index 0000000..a12cf87 --- /dev/null +++ b/src/api/resources/incident.test.js @@ -0,0 +1,56 @@ +import { describe, it, vi, beforeEach } from "vitest"; + +vi.mock("../helper.jsx", () => ({ + DELETE: vi.fn(), + GET: vi.fn(), + PUT: vi.fn(), +})); + +import { DELETE, GET, PUT } from "../helper.jsx"; +import { create_mock_state, expect_api_call } from "../../test/helpers.js"; +import incident from "./incident.js"; + +describe("api/resources/incident", () => { + let state; + beforeEach(() => { + state = create_mock_state(); + }); + + it("by_process_definition() GETs runtime incidents filtered by definition", () => { + incident.by_process_definition(state, "def-1"); + expect_api_call(GET, { + url: "/incident?processDefinitionId=def-1", + state, + signal: state.api.incident.by_process_definition, + }); + }); + + it("by_process_instance() GETs runtime incidents filtered by instance", () => { + incident.by_process_instance(state, "inst-1"); + expect_api_call(GET, { + url: "/incident?processInstanceId=inst-1", + state, + signal: state.api.incident.by_process_instance, + }); + }); + + it("set_annotation() PUTs the annotation body", () => { + incident.set_annotation(state, "inc-1", "Checked by ops"); + expect_api_call(PUT, { + url: "/incident/inc-1/annotation", + body: { annotation: "Checked by ops" }, + state, + signal: state.api.incident.annotation, + }); + }); + + it("clear_annotation() DELETEs the annotation", () => { + incident.clear_annotation(state, "inc-1"); + expect_api_call(DELETE, { + url: "/incident/inc-1/annotation", + body: null, + state, + signal: state.api.incident.annotation, + }); + }); +}); diff --git a/src/css/components.css b/src/css/components.css index 5bed3e4..b9e1c7c 100644 --- a/src/css/components.css +++ b/src/css/components.css @@ -533,6 +533,23 @@ main#processes { max-width: 40%; } +#processes form.incident-annotation { + display: flex; + align-items: center; + gap: var(--spacing-1); + margin: 0; +} + +#processes form.incident-annotation input { + min-width: 14rem; +} + +#processes form.incident-annotation .button-group { + grid-column: auto; + justify-content: flex-start; + flex-wrap: nowrap; +} + /* Definition / instance metadata renders as a
grid. */ #processes dl { display: grid; diff --git a/src/pages/Processes.jsx b/src/pages/Processes.jsx index d6ac79e..5e5fc4b 100644 --- a/src/pages/Processes.jsx +++ b/src/pages/Processes.jsx @@ -167,6 +167,9 @@ const ProcessesPage = () => { state.api.process.instance.list.value = null; state.api.process.instance.one.value = null; state.api.process.instance.activity_instances.value = null; + state.api.incident.by_process_definition.value = null; + state.api.incident.by_process_instance.value = null; + state.api.incident.annotation.value = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.definition_id]); @@ -686,7 +689,7 @@ const DefinitionsEmpty = () => { {t("processes.empty.how-to")} @@ -928,8 +931,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 ( @@ -1005,6 +1007,83 @@ const ProcessInstance = ({ id, startTime, state, businessKey }) => { ); }; +const refetch_definition_incidents = (state, definition_id, history_mode) => + history_mode + ? engine_rest.history.incident.by_process_definition(state, definition_id) + : engine_rest.incident.by_process_definition(state, definition_id); + +const refetch_instance_incidents = (state, instance_id, history_mode) => + history_mode + ? engine_rest.history.incident.by_process_instance(state, instance_id) + : engine_rest.incident.by_process_instance(state, instance_id); + +const IncidentAnnotationActions = ({ incident, readonly, on_refresh }) => { + const state = useContext(AppState), + [t] = useTranslation(), + annotation = useSignal(incident.annotation ?? ""), + saving = useSignal(false); + + useEffect(() => { + annotation.value = incident.annotation ?? ""; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [incident.id, incident.annotation]); + + const submit = async (event) => { + event.preventDefault(); + saving.value = true; + try { + await engine_rest.incident.set_annotation( + state, + incident.id, + annotation.value, + ); + await on_refresh?.(); + } finally { + saving.value = false; + } + }, + clear = async () => { + saving.value = true; + try { + await engine_rest.incident.clear_annotation(state, incident.id); + annotation.value = ""; + await on_refresh?.(); + } finally { + saving.value = false; + } + }; + + if (readonly) return annotation.value || "—"; + + return ( +
+ (annotation.value = event.currentTarget.value)} + /> + + + {incident.annotation ? ( + + ) : null} + +
+ ); +}; + const InstanceVariables = () => { const state = useContext(AppState), { params, query } = useRoute(), @@ -1044,9 +1123,8 @@ const InstanceVariables = () => { ? Object.entries( state.api.process.instance.variables.value.data, ).map( - // eslint-disable-next-line react/jsx-key ([name, { type, value }]) => ( - + {name} {type} {value} @@ -1054,9 +1132,8 @@ const InstanceVariables = () => { ), ) : state.api.process.instance.variables.value.data.map( - // eslint-disable-next-line react/jsx-key ({ name, type, value }) => ( - + {name} {type} {value} @@ -1074,16 +1151,13 @@ const InstanceIncidents = () => { const state = useContext(AppState), { params, query } = useRoute(), history_mode = query.history === "true", - [t] = useTranslation(); + [t] = useTranslation(), + incidents = history_mode + ? state.api.history.incident.by_process_instance + : state.api.incident.by_process_instance; - // The incident endpoint is history-only in Camunda 7, but timestamp filters - // applied via history mode (e.g. closed instance window) can change the - // returned set — re-fetch on toggle. useEffect(() => { - void engine_rest.history.incident.by_process_instance( - state, - params.selection_id, - ); + refetch_instance_incidents(state, params.selection_id, history_mode); // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.selection_id, history_mode]); @@ -1102,16 +1176,16 @@ const InstanceIncidents = () => { {t("processes.incidents.root-cause-process-instance-id")} {t("common.type")} {t("processes.incidents.annotation")} - {t("common.action")} - {state.api.history.incident.by_process_instance.value?.data?.map( + {incidents.value?.data?.map( // eslint-disable-next-line react/jsx-key ({ id, incidentMessage, processInstanceId, + incidentTimestamp, createTime, activityId, failedActivityId, @@ -1126,8 +1200,10 @@ const InstanceIncidents = () => { -