mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 22:58:09 +00:00
moved some more files w/ burnettk
This commit is contained in:
parent
559589a5b7
commit
5cdfa5a135
@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
import { Alert, AlertTitle } from '@mui/material';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import {
|
||||
ErrorForDisplay,
|
||||
ProcessInstanceEventErrorDetail,
|
||||
ProcessInstanceLogEntry,
|
||||
TestCaseErrorDetails,
|
||||
} from '../interfaces';
|
||||
|
||||
const defaultMessageClassName = 'failure-string';
|
||||
|
||||
function errorDetailDisplay(
|
||||
errorObject: any,
|
||||
propertyName: string,
|
||||
title: string,
|
||||
) {
|
||||
// Creates a bit of html for displaying a single error property if it exists.
|
||||
let value = errorObject[propertyName];
|
||||
if (propertyName in errorObject && value) {
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
return (
|
||||
<div className="error_info">
|
||||
<span className="error_title">{title}:</span>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const errorForDisplayFromString = (errorMessage: string) => {
|
||||
const errorForDisplay: ErrorForDisplay = {
|
||||
message: errorMessage,
|
||||
messageClassName: defaultMessageClassName,
|
||||
};
|
||||
return errorForDisplay;
|
||||
};
|
||||
|
||||
export const errorForDisplayFromProcessInstanceErrorDetail = (
|
||||
processInstanceEvent: ProcessInstanceLogEntry,
|
||||
processInstanceErrorEventDetail: ProcessInstanceEventErrorDetail,
|
||||
) => {
|
||||
const errorForDisplay: ErrorForDisplay = {
|
||||
message: processInstanceErrorEventDetail.message,
|
||||
messageClassName: defaultMessageClassName,
|
||||
task_name: processInstanceEvent.task_definition_name,
|
||||
task_id: processInstanceEvent.task_definition_identifier,
|
||||
line_number: processInstanceErrorEventDetail.task_line_number,
|
||||
error_line: processInstanceErrorEventDetail.task_line_contents,
|
||||
task_trace: processInstanceErrorEventDetail.task_trace,
|
||||
stacktrace: processInstanceErrorEventDetail.stacktrace,
|
||||
};
|
||||
return errorForDisplay;
|
||||
};
|
||||
|
||||
export const errorForDisplayFromTestCaseErrorDetails = (
|
||||
testCaseErrorDetails: TestCaseErrorDetails,
|
||||
) => {
|
||||
const errorForDisplay: ErrorForDisplay = {
|
||||
message: testCaseErrorDetails.error_messages.join('\n'),
|
||||
messageClassName: defaultMessageClassName,
|
||||
task_name: testCaseErrorDetails.task_bpmn_name,
|
||||
task_id: testCaseErrorDetails.task_bpmn_identifier,
|
||||
line_number: testCaseErrorDetails.task_line_number,
|
||||
error_line: testCaseErrorDetails.task_line_contents,
|
||||
task_trace: testCaseErrorDetails.task_trace,
|
||||
stacktrace: testCaseErrorDetails.stacktrace,
|
||||
|
||||
task_type: testCaseErrorDetails.task_bpmn_type,
|
||||
output_data: testCaseErrorDetails.output_data,
|
||||
expected_data: testCaseErrorDetails.expected_data,
|
||||
};
|
||||
return errorForDisplay;
|
||||
};
|
||||
|
||||
export const childrenForErrorObject = (errorObject: ErrorForDisplay) => {
|
||||
let sentryLinkTag = null;
|
||||
if (errorObject.sentry_link) {
|
||||
sentryLinkTag = (
|
||||
<span>
|
||||
{
|
||||
': Find details about this error here (it may take a moment to become available): '
|
||||
}
|
||||
<a href={errorObject.sentry_link} target="_blank" rel="noreferrer">
|
||||
{errorObject.sentry_link}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const message = (
|
||||
<div className={errorObject.messageClassName}>{errorObject.message}</div>
|
||||
);
|
||||
const taskName = errorDetailDisplay(errorObject, 'task_name', 'Task Name');
|
||||
const taskId = errorDetailDisplay(errorObject, 'task_id', 'Task ID');
|
||||
const fileName = errorDetailDisplay(errorObject, 'file_name', 'File Name');
|
||||
const lineNumber = errorDetailDisplay(
|
||||
errorObject,
|
||||
'line_number',
|
||||
'Line Number',
|
||||
);
|
||||
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
|
||||
const taskType = errorDetailDisplay(errorObject, 'task_type', 'Task Type');
|
||||
const outputData = errorDetailDisplay(
|
||||
errorObject,
|
||||
'output_data',
|
||||
'Output Data',
|
||||
);
|
||||
const expectedData = errorDetailDisplay(
|
||||
errorObject,
|
||||
'expected_data',
|
||||
'Expected Data',
|
||||
);
|
||||
let codeTrace = null;
|
||||
if (errorObject.task_trace && errorObject.task_trace.length > 0) {
|
||||
codeTrace = (
|
||||
<div className="error_info">
|
||||
<span className="error_title">Call Activity Trace:</span>
|
||||
{errorObject.task_trace.reverse().join(' -> ')}
|
||||
</div>
|
||||
);
|
||||
} else if (errorObject.stacktrace) {
|
||||
codeTrace = (
|
||||
<pre className="error_info">
|
||||
<span className="error_title">Stacktrace:</span>
|
||||
{errorObject.stacktrace.reverse().map((a) => (
|
||||
<>
|
||||
{a}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
message,
|
||||
<br />,
|
||||
sentryLinkTag,
|
||||
taskName,
|
||||
taskId,
|
||||
fileName,
|
||||
lineNumber,
|
||||
errorLine,
|
||||
codeTrace,
|
||||
taskType,
|
||||
outputData,
|
||||
expectedData,
|
||||
];
|
||||
};
|
||||
|
||||
export function errorDisplayStateless(
|
||||
errorObject: ErrorForDisplay,
|
||||
onClose?: Function,
|
||||
) {
|
||||
const title = 'Error:';
|
||||
const hideCloseButton = !onClose;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
onClose={onClose}
|
||||
action={hideCloseButton ? null : <button onClick={onClose}>Close</button>}
|
||||
>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
{childrenForErrorObject(errorObject)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ErrorDisplay() {
|
||||
const errorObject = useAPIError().error;
|
||||
const { removeError } = useAPIError();
|
||||
let errorTag = null;
|
||||
|
||||
if (errorObject) {
|
||||
errorTag = errorDisplayStateless(errorObject, removeError);
|
||||
}
|
||||
return errorTag;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import UserService from '../services/UserService';
|
||||
|
||||
export default function LoginHandler() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!UserService.isLoggedIn()) {
|
||||
navigate(`/login?original_url=${UserService.getCurrentLocation()}`);
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import FormattingService from '../services/FormattingService';
|
||||
|
||||
export default function MarkdownRenderer(props: any) {
|
||||
const { source } = props;
|
||||
const newMarkdown = FormattingService.checkForSpiffFormats(source);
|
||||
let wrapperClassName = '';
|
||||
const propsToUse = props;
|
||||
if ('wrapperClassName' in propsToUse) {
|
||||
wrapperClassName = propsToUse.wrapperClassName;
|
||||
delete propsToUse.wrapperClassName;
|
||||
}
|
||||
return (
|
||||
<div data-color-mode="light" className={wrapperClassName}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<MDEditor.Markdown {...{ ...propsToUse, ...{ source: newMarkdown } }} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -15,7 +15,7 @@ import InstructionsForEndUser from './InstructionsForEndUser';
|
||||
import {
|
||||
errorDisplayStateless,
|
||||
errorForDisplayFromProcessInstanceErrorDetail,
|
||||
} from '../../components/ErrorDisplay';
|
||||
} from './ErrorDisplay';
|
||||
|
||||
type OwnProps = {
|
||||
processInstanceId: number;
|
||||
|
@ -0,0 +1,165 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Button from '@mui/material/Button'; // Updated import for MUI Button
|
||||
import { Can } from '@casl/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
ProcessInstance,
|
||||
ProcessModel,
|
||||
RecentProcessModel,
|
||||
} from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
const storeRecentProcessModelInLocalStorage = (
|
||||
processModelForStorage: ProcessModel,
|
||||
) => {
|
||||
// All values stored in localStorage are strings.
|
||||
// Grab our recentProcessModels string from localStorage.
|
||||
const stringFromLocalStorage = window.localStorage.getItem(
|
||||
'recentProcessModels',
|
||||
);
|
||||
|
||||
// adapted from https://stackoverflow.com/a/59424458/6090676
|
||||
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
|
||||
let array: RecentProcessModel[] = [];
|
||||
if (stringFromLocalStorage !== null) {
|
||||
// Then parse that string into an actual value.
|
||||
array = JSON.parse(stringFromLocalStorage);
|
||||
}
|
||||
|
||||
// Here's the value we want to add
|
||||
const value = {
|
||||
processModelIdentifier: processModelForStorage.id,
|
||||
processModelDisplayName: processModelForStorage.display_name,
|
||||
};
|
||||
|
||||
// anything with a processGroupIdentifier is old and busted. leave it behind.
|
||||
array = array.filter((item) => item.processGroupIdentifier === undefined);
|
||||
|
||||
// If our parsed/empty array doesn't already have this value in it...
|
||||
const matchingItem = array.find(
|
||||
(item) => item.processModelIdentifier === value.processModelIdentifier,
|
||||
);
|
||||
if (matchingItem === undefined) {
|
||||
// add the value to the beginning of the array
|
||||
array.unshift(value);
|
||||
|
||||
// Keep the array to 3 items
|
||||
if (array.length > 3) {
|
||||
array.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
|
||||
|
||||
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
|
||||
const stringRepresentingArray = JSON.stringify(array);
|
||||
|
||||
// and store it in localStorage as "recentProcessModels"
|
||||
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
processModel: ProcessModel;
|
||||
className?: string;
|
||||
checkPermissions?: boolean;
|
||||
buttonText?: string;
|
||||
};
|
||||
|
||||
export default function ProcessInstanceRun({
|
||||
processModel,
|
||||
className,
|
||||
checkPermissions = true,
|
||||
buttonText = 'Start',
|
||||
}: OwnProps) {
|
||||
const navigate = useNavigate();
|
||||
const { addError, removeError } = useAPIError();
|
||||
const [disableStartButton, setDisableStartButton] = useState<boolean>(false);
|
||||
|
||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||
processModel.id,
|
||||
);
|
||||
|
||||
const processInstanceCreatePath = `/v1.0/process-instances/${modifiedProcessModelId}`;
|
||||
let permissionRequestData: PermissionsToCheck = {
|
||||
[processInstanceCreatePath]: ['POST'],
|
||||
};
|
||||
|
||||
if (!checkPermissions) {
|
||||
permissionRequestData = {};
|
||||
}
|
||||
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
const onProcessInstanceRun = (processInstance: ProcessInstance) => {
|
||||
const processInstanceId = processInstance.id;
|
||||
if (processInstance.process_model_uses_queued_execution) {
|
||||
navigate(
|
||||
`/process-instances/for-me/${modifiedProcessModelId}/${processInstanceId}/progress`,
|
||||
);
|
||||
} else {
|
||||
navigate(
|
||||
`/process-instances/for-me/${modifiedProcessModelId}/${processInstanceId}/interstitial`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const processModelRun = (processInstance: ProcessInstance) => {
|
||||
removeError();
|
||||
if (processModel) {
|
||||
storeRecentProcessModelInLocalStorage(processModel);
|
||||
}
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
|
||||
successCallback: onProcessInstanceRun,
|
||||
failureCallback: (result: any) => {
|
||||
addError(result);
|
||||
setDisableStartButton(false);
|
||||
},
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
const processInstanceCreateAndRun = () => {
|
||||
removeError();
|
||||
setDisableStartButton(true);
|
||||
HttpService.makeCallToBackend({
|
||||
path: processInstanceCreatePath,
|
||||
successCallback: processModelRun,
|
||||
failureCallback: (result: any) => {
|
||||
addError(result);
|
||||
setDisableStartButton(false);
|
||||
},
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
let startButton = null;
|
||||
if (processModel.primary_file_name && processModel.is_executable) {
|
||||
startButton = (
|
||||
<Button
|
||||
data-qa="start-process-instance"
|
||||
onClick={processInstanceCreateAndRun}
|
||||
className={className}
|
||||
disabled={disableStartButton}
|
||||
variant="contained" // MUI specific prop
|
||||
size="medium" // MUI specific prop
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// if checkPermissions is false then assume the page using this component has already checked the permissions
|
||||
if (checkPermissions) {
|
||||
return (
|
||||
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
|
||||
{startButton}
|
||||
</Can>
|
||||
);
|
||||
}
|
||||
return startButton;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Tabs, Tab } from '@mui/material'; // Importing MUI components
|
||||
import { SpiffTab } from '../interfaces';
|
||||
|
||||
type OwnProps = {
|
||||
tabs: SpiffTab[];
|
||||
};
|
||||
|
||||
export default function SpiffTabs({ tabs }: OwnProps) {
|
||||
const location = useLocation();
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState<number | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
let newSelectedTabIndex = tabs.findIndex((spiffTab: SpiffTab) => {
|
||||
return location.pathname === spiffTab.path;
|
||||
});
|
||||
if (newSelectedTabIndex === -1) {
|
||||
newSelectedTabIndex = 0;
|
||||
}
|
||||
setSelectedTabIndex(newSelectedTabIndex);
|
||||
}, [location, tabs]);
|
||||
|
||||
const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
navigate(tabs[newValue].path);
|
||||
};
|
||||
|
||||
if (selectedTabIndex !== null && tabs.length > selectedTabIndex) {
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
value={selectedTabIndex}
|
||||
onChange={handleTabChange}
|
||||
aria-label="List of tabs"
|
||||
>
|
||||
{tabs.map((spiffTab: SpiffTab, index: number) => (
|
||||
<Tab key={index} label={spiffTab.display_name} />
|
||||
))}
|
||||
</Tabs>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
// src/common/hooks/useAPIError/index.js
|
||||
import { useContext } from 'react';
|
||||
// Import MUI components if needed
|
||||
// import { SomeMUIComponent } from '@mui/material';
|
||||
import { APIErrorContext } from '../contexts/APIErrorContext';
|
||||
|
||||
function useAPIError() {
|
||||
const { error, addError, removeError } = useContext(APIErrorContext);
|
||||
return { error, addError, removeError };
|
||||
}
|
||||
|
||||
export default useAPIError;
|
@ -0,0 +1,33 @@
|
||||
import DateAndTimeService from './DateAndTimeService';
|
||||
|
||||
const spiffFormatFunctions: { [key: string]: Function } = {
|
||||
convert_seconds_to_date_time_for_display: DateAndTimeService.formatDateTime,
|
||||
convert_seconds_to_duration_for_display:
|
||||
DateAndTimeService.formatDurationForDisplay,
|
||||
convert_date_to_date_for_display:
|
||||
DateAndTimeService.ymdDateStringToConfiguredFormat,
|
||||
};
|
||||
|
||||
const checkForSpiffFormats = (markdown: string) => {
|
||||
const replacer = (
|
||||
match: string,
|
||||
spiffFormat: string,
|
||||
originalValue: string,
|
||||
) => {
|
||||
if (spiffFormat in spiffFormatFunctions) {
|
||||
return spiffFormatFunctions[spiffFormat](originalValue);
|
||||
}
|
||||
console.warn(
|
||||
`attempted: ${match}, but ${spiffFormat} is not a valid conversion function`,
|
||||
);
|
||||
|
||||
return match;
|
||||
};
|
||||
return markdown.replaceAll(/SPIFF_FORMAT:::(\w+)\(([^)]+)\)/g, replacer);
|
||||
};
|
||||
|
||||
const FormattingService = {
|
||||
checkForSpiffFormats,
|
||||
};
|
||||
|
||||
export default FormattingService;
|
@ -22,8 +22,8 @@ import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
import LoginHandler from '../components/LoginHandler';
|
||||
import SpiffTabs from '../components/SpiffTabs';
|
||||
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||
import CreateNewInstance from './CreateNewInstance';
|
||||
// import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||
// import CreateNewInstance from './CreateNewInstance';
|
||||
|
||||
type OwnProps = {
|
||||
pageIdentifier?: string;
|
||||
@ -63,10 +63,10 @@ export default function Extension({
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
const supportedComponents: SupportedComponentList = {
|
||||
CreateNewInstance,
|
||||
// CreateNewInstance,
|
||||
CustomForm,
|
||||
MarkdownRenderer,
|
||||
ProcessInstanceListTable,
|
||||
// ProcessInstanceListTable,
|
||||
ProcessInstanceRun,
|
||||
SpiffTabs,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user