added api to get error details for an event and added simple modal on frontend to show it

This commit is contained in:
jasquat 2023-04-19 13:56:00 -04:00
parent 320d1b4083
commit 6747d9df3d
8 changed files with 174 additions and 11 deletions

View File

@ -2010,6 +2010,39 @@ paths:
schema: schema:
$ref: "#/components/schemas/ProcessInstanceLog" $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: /secrets:
parameters: parameters:
- name: page - name: page

View File

@ -1,9 +1,11 @@
from dataclasses import dataclass
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
@dataclass
class ProcessInstanceErrorDetailModel(SpiffworkflowBaseDBModel): class ProcessInstanceErrorDetailModel(SpiffworkflowBaseDBModel):
__tablename__ = "process_instance_error_detail" __tablename__ = "process_instance_error_detail"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)

View File

@ -39,7 +39,7 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel):
user_id = db.Column(ForeignKey(UserModel.id), nullable=True, index=True) # type: ignore 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") @validates("event_type")
def validate_event_type(self, key: str, value: Any) -> Any: def validate_event_type(self, key: str, value: Any) -> Any:

View File

@ -1,4 +1,5 @@
from typing import Optional from typing import Optional
from spiffworkflow_backend.exceptions.api_error import ApiError
import flask.wrappers import flask.wrappers
from flask import jsonify from flask import jsonify
@ -91,3 +92,20 @@ def types() -> flask.wrappers.Response:
task_types = [t.typename for t in query] task_types = [t.typename for t in query]
event_types = ProcessInstanceEventType.list() event_types = ProcessInstanceEventType.list()
return make_response(jsonify({"task_types": task_types, "event_types": event_types}), 200) 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)

View File

@ -10,18 +10,19 @@ export const useUriListForPermissions = () => {
processGroupListPath: '/v1.0/process-groups', processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_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}`, 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', processInstanceListPath: '/v1.0/process-instances',
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
processInstanceReportListPath: '/v1.0/process-instances/reports', 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}`, 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}`, 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}`, processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`,
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, 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`, 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}`, processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`,
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`, processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`, processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,

View File

@ -297,6 +297,12 @@ export interface JsonSchemaForm {
required: string[]; required: string[];
} }
export interface ProcessInstanceEventErrorDetail {
id: number;
message: string;
stacktrace: string;
}
export interface ProcessInstanceLogEntry { export interface ProcessInstanceLogEntry {
bpmn_process_definition_identifier: string; bpmn_process_definition_identifier: string;
bpmn_process_definition_name: string; bpmn_process_definition_name: string;

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ErrorOutline } from '@carbon/icons-react';
import { import {
Table, Table,
Tabs, Tabs,
@ -10,6 +11,8 @@ import {
Button, Button,
TextInput, TextInput,
ComboBox, ComboBox,
Modal,
Loading,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { import {
@ -28,8 +31,13 @@ import {
} from '../helpers'; } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { ProcessInstanceLogEntry } from '../interfaces'; import {
PermissionsToCheck,
ProcessInstanceEventErrorDetail,
ProcessInstanceLogEntry,
} from '../interfaces';
import Filters from '../components/Filters'; import Filters from '../components/Filters';
import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = { type OwnProps = {
variant: string; variant: string;
@ -46,11 +54,16 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
const [taskTypes, setTaskTypes] = useState<string[]>([]); const [taskTypes, setTaskTypes] = useState<string[]>([]);
const [eventTypes, setEventTypes] = useState<string[]>([]); const [eventTypes, setEventTypes] = useState<string[]>([]);
const [eventForModal, setEventForModal] =
useState<ProcessInstanceLogEntry | null>(null);
const [eventErrorDetails, setEventErrorDetails] =
useState<ProcessInstanceEventErrorDetail | null>(null);
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const isDetailedView = searchParams.get('detailed') === 'true'; const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceErrorEventDetails]: ['GET'],
const taskNameHeader = isDetailedView ? 'Task Name' : 'Milestone'; };
const { ability } = usePermissionFetcher(permissionRequestData);
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false); const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
@ -58,6 +71,8 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
if (variant === 'all') { if (variant === 'all') {
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`; 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) => { const updateSearchParams = (value: string, key: string) => {
if (value) { if (value) {
@ -128,6 +143,92 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
isDetailedView, isDetailedView,
]); ]);
const handleErrorEventModalClose = () => {
setEventForModal(null);
setEventErrorDetails(null);
};
const errorEventModal = () => {
if (eventForModal) {
const modalHeading = `Event Error Details for`;
let errorMessageTag = (
<Loading className="some-class" withOverlay={false} small />
);
if (eventErrorDetails) {
errorMessageTag = (
<>
<p className="failure-string">{eventErrorDetails.message} NOOO</p>
<br />
<pre>{eventErrorDetails.stacktrace}</pre>
</>
);
}
return (
<Modal
open={!!eventForModal}
passiveModal
onRequestClose={handleErrorEventModalClose}
modalHeading={modalHeading}
modalLabel="Error Details"
>
{errorMessageTag}
</Modal>
);
}
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 = (
<>
&nbsp;
<ErrorOutline className="red-icon" />
</>
);
return (
<Button
kind="ghost"
className="button-link"
onClick={() => getErrorDetailsForEvent(logEntry)}
title={errorTitle}
>
{logEntry.event_type}
{errorIcon}
</Button>
);
}
return logEntry.event_type;
};
const getTableRow = (logEntry: ProcessInstanceLogEntry) => { const getTableRow = (logEntry: ProcessInstanceLogEntry) => {
const tableRow = []; const tableRow = [];
const taskNameCell = ( const taskNameCell = (
@ -164,7 +265,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
<> <>
<td>{logEntry.task_definition_identifier}</td> <td>{logEntry.task_definition_identifier}</td>
<td>{logEntry.bpmn_task_type}</td> <td>{logEntry.bpmn_task_type}</td>
<td>{logEntry.event_type}</td> <td>{eventTypeCell(logEntry)}</td>
<td> <td>
{logEntry.username || ( {logEntry.username || (
<span className="system-user-log-entry">system</span> <span className="system-user-log-entry">system</span>
@ -405,6 +506,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
]} ]}
/> />
{tabs()} {tabs()}
{errorEventModal()}
<Filters <Filters
filterOptions={filterOptions} filterOptions={filterOptions}
showFilterOptions={showFilterOptions} showFilterOptions={showFilterOptions}

View File

@ -1,2 +1,3 @@
// carbon/react is not very typescript safe so ignore it // carbon/react is not very typescript safe so ignore it
declare module '@carbon/react'; declare module '@carbon/react';
declare module '@carbon/icons-react';