added api to get error details for an event and added simple modal on frontend to show it
This commit is contained in:
parent
320d1b4083
commit
6747d9df3d
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue