diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index c14550130..131bce354 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -2010,6 +2010,39 @@ paths: schema: $ref: "#/components/schemas/ProcessInstanceLog" + /event-error-details/{modified_process_model_identifier}/{process_instance_id}/{process_instance_event_id}: + parameters: + - name: process_instance_id + in: path + required: true + description: the id of the process instance + schema: + type: integer + - name: modified_process_model_identifier + in: path + required: true + description: The process_model_id, modified to replace slashes (/) + schema: + type: string + - name: process_instance_event_id + in: path + required: true + description: the id of the process instance event + schema: + type: integer + get: + tags: + - Process Instance Events + operationId: spiffworkflow_backend.routes.process_instance_events_controller.error_details + summary: returns the error details for a given process instance event. + responses: + "200": + description: list of types + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessInstanceLog" + /secrets: parameters: - name: page diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_error_detail.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_error_detail.py index 91324c772..d1a38c49e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_error_detail.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_error_detail.py @@ -1,9 +1,11 @@ +from dataclasses import dataclass from sqlalchemy.orm import relationship from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from sqlalchemy import ForeignKey from spiffworkflow_backend.models.db import db +@dataclass class ProcessInstanceErrorDetailModel(SpiffworkflowBaseDBModel): __tablename__ = "process_instance_error_detail" id: int = db.Column(db.Integer, primary_key=True) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py index 936e4d9d3..0b3738169 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py @@ -39,7 +39,7 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel): user_id = db.Column(ForeignKey(UserModel.id), nullable=True, index=True) # type: ignore - error_deatils = relationship("ProcessInstanceErrorDetailModel", cascade="delete") # type: ignore + error_details = relationship("ProcessInstanceErrorDetailModel", cascade="delete") # type: ignore @validates("event_type") def validate_event_type(self, key: str, value: Any) -> Any: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py index 5711f6bcc..d829434e7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py @@ -1,4 +1,5 @@ from typing import Optional +from spiffworkflow_backend.exceptions.api_error import ApiError import flask.wrappers from flask import jsonify @@ -91,3 +92,20 @@ def types() -> flask.wrappers.Response: task_types = [t.typename for t in query] event_types = ProcessInstanceEventType.list() return make_response(jsonify({"task_types": task_types, "event_types": event_types}), 200) + + +def error_details( + modified_process_model_identifier: str, + process_instance_id: int, + process_instance_event_id: int, +) -> flask.wrappers.Response: + process_instance_event = ProcessInstanceEventModel.query.filter_by(id=process_instance_event_id).first() + if process_instance_event is None: + raise ( + ApiError( + error_code="process_instance_event_cannot_be_found", + message=f"Process instance event cannot be found: {process_instance_event_id}", + status_code=400, + ) + ) + return make_response(jsonify(process_instance_event.error_details[0]), 200) diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx index c33946985..ae663541b 100644 --- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx +++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx @@ -10,18 +10,19 @@ export const useUriListForPermissions = () => { processGroupListPath: '/v1.0/process-groups', processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`, + processInstanceCompleteTaskPath: `/v1.0/complete-task/${params.process_model_id}/${params.process_instance_id}`, processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`, + processInstanceErrorEventDetails: `/v1.0/event-error-details/${params.process_model_id}/${params.process_instance_id}`, processInstanceListPath: '/v1.0/process-instances', processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceReportListPath: '/v1.0/process-instances/reports', - processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, - processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`, processInstanceResetPath: `/v1.0/process-instance-reset/${params.process_model_id}/${params.process_instance_id}`, - processInstanceTaskDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`, + processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, processInstanceSendEventPath: `/v1.0/send-event/${params.process_model_id}/${params.process_instance_id}`, - processInstanceCompleteTaskPath: `/v1.0/complete-task/${params.process_model_id}/${params.process_instance_id}`, - processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, + processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`, + processInstanceTaskDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`, processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`, + processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`, processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`, processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`, diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 802c48c7d..c26c1ce32 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -297,6 +297,12 @@ export interface JsonSchemaForm { required: string[]; } +export interface ProcessInstanceEventErrorDetail { + id: number; + message: string; + stacktrace: string; +} + export interface ProcessInstanceLogEntry { bpmn_process_definition_identifier: string; bpmn_process_definition_name: string; diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx index d32a5b347..16bb3f685 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { ErrorOutline } from '@carbon/icons-react'; import { Table, Tabs, @@ -10,6 +11,8 @@ import { Button, TextInput, ComboBox, + Modal, + Loading, // @ts-ignore } from '@carbon/react'; import { @@ -28,8 +31,13 @@ import { } from '../helpers'; import HttpService from '../services/HttpService'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; -import { ProcessInstanceLogEntry } from '../interfaces'; +import { + PermissionsToCheck, + ProcessInstanceEventErrorDetail, + ProcessInstanceLogEntry, +} from '../interfaces'; import Filters from '../components/Filters'; +import { usePermissionFetcher } from '../hooks/PermissionService'; type OwnProps = { variant: string; @@ -46,11 +54,16 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { const [taskTypes, setTaskTypes] = useState([]); const [eventTypes, setEventTypes] = useState([]); + const [eventForModal, setEventForModal] = + useState(null); + const [eventErrorDetails, setEventErrorDetails] = + useState(null); const { targetUris } = useUriListForPermissions(); - const isDetailedView = searchParams.get('detailed') === 'true'; - - const taskNameHeader = isDetailedView ? 'Task Name' : 'Milestone'; + const permissionRequestData: PermissionsToCheck = { + [targetUris.processInstanceErrorEventDetails]: ['GET'], + }; + const { ability } = usePermissionFetcher(permissionRequestData); const [showFilterOptions, setShowFilterOptions] = useState(false); @@ -58,6 +71,8 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { if (variant === 'all') { processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`; } + const isDetailedView = searchParams.get('detailed') === 'true'; + const taskNameHeader = isDetailedView ? 'Task Name' : 'Milestone'; const updateSearchParams = (value: string, key: string) => { if (value) { @@ -128,6 +143,92 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { isDetailedView, ]); + const handleErrorEventModalClose = () => { + setEventForModal(null); + setEventErrorDetails(null); + }; + + const errorEventModal = () => { + if (eventForModal) { + const modalHeading = `Event Error Details for`; + let errorMessageTag = ( + + ); + if (eventErrorDetails) { + errorMessageTag = ( + <> +

{eventErrorDetails.message} NOOO

+
+
{eventErrorDetails.stacktrace}
+ + ); + } + return ( + + {errorMessageTag} + + ); + } + return null; + }; + + const handleErrorDetailsReponse = ( + results: ProcessInstanceEventErrorDetail + ) => { + setEventErrorDetails(results); + }; + + const getErrorDetailsForEvent = (logEntry: ProcessInstanceLogEntry) => { + setEventForModal(logEntry); + if (ability.can('GET', targetUris.processInstanceErrorEventDetails)) { + HttpService.makeCallToBackend({ + path: `${targetUris.processInstanceErrorEventDetails}/${logEntry.id}`, + httpMethod: 'GET', + successCallback: handleErrorDetailsReponse, + failureCallback: (error: any) => { + const errorObject: ProcessInstanceEventErrorDetail = { + id: 0, + message: `ERROR: ${error.message}`, + stacktrace: '', + }; + setEventErrorDetails(errorObject); + }, + }); + } + }; + + const eventTypeCell = (logEntry: ProcessInstanceLogEntry) => { + if ( + ['process_instance_error', 'task_error'].includes(logEntry.event_type) + ) { + const errorTitle = 'Event has an error'; + const errorIcon = ( + <> +   + + + ); + return ( + + ); + } + return logEntry.event_type; + }; + const getTableRow = (logEntry: ProcessInstanceLogEntry) => { const tableRow = []; const taskNameCell = ( @@ -164,7 +265,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { <> {logEntry.task_definition_identifier} {logEntry.bpmn_task_type} - {logEntry.event_type} + {eventTypeCell(logEntry)} {logEntry.username || ( system @@ -405,6 +506,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { ]} /> {tabs()} + {errorEventModal()}