mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 22:58:09 +00:00
moved some components to new ui w/ burnettk
This commit is contained in:
parent
818fbcb217
commit
f018faf505
@ -0,0 +1,211 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { ErrorOutline } from '@carbon/icons-react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Table, Modal, Button } from '@carbon/react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import PaginationForTable from '../PaginationForTable';
|
||||||
|
import ProcessBreadcrumb from '../ProcessBreadcrumb';
|
||||||
|
import {
|
||||||
|
getPageInfoFromSearchParams,
|
||||||
|
modifyProcessIdentifierForPathParam,
|
||||||
|
} from '../../helpers';
|
||||||
|
import HttpService from '../../services/HttpService';
|
||||||
|
import { FormatProcessModelDisplayName } from '../MiniComponents';
|
||||||
|
import { MessageInstance } from '../../interfaces';
|
||||||
|
import DateAndTimeService from '../../services/DateAndTimeService';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
processInstanceId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationQueryParamPrefix = 'message-list';
|
||||||
|
|
||||||
|
export default function MessageInstanceList({ processInstanceId }: OwnProps) {
|
||||||
|
const [messageInstances, setMessageInstances] = useState([]);
|
||||||
|
const [pagination, setPagination] = useState(null);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [messageInstanceForModal, setMessageInstanceForModal] =
|
||||||
|
useState<MessageInstance | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setMessageInstanceListFromResult = (result: any) => {
|
||||||
|
setMessageInstances(result.results);
|
||||||
|
setPagination(result.pagination);
|
||||||
|
};
|
||||||
|
const { page, perPage } = getPageInfoFromSearchParams(
|
||||||
|
searchParams,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
paginationQueryParamPrefix,
|
||||||
|
);
|
||||||
|
let queryParamString = `per_page=${perPage}&page=${page}`;
|
||||||
|
if (processInstanceId) {
|
||||||
|
queryParamString += `&process_instance_id=${processInstanceId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/messages?${queryParamString}`,
|
||||||
|
successCallback: setMessageInstanceListFromResult,
|
||||||
|
});
|
||||||
|
}, [processInstanceId, searchParams]);
|
||||||
|
|
||||||
|
const handleCorrelationDisplayClose = () => {
|
||||||
|
setMessageInstanceForModal(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const correlationsDisplayModal = () => {
|
||||||
|
if (messageInstanceForModal) {
|
||||||
|
let failureCausePre = null;
|
||||||
|
if (messageInstanceForModal.failure_cause) {
|
||||||
|
failureCausePre = (
|
||||||
|
<>
|
||||||
|
<p className="failure-string">
|
||||||
|
{messageInstanceForModal.failure_cause}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={!!messageInstanceForModal}
|
||||||
|
passiveModal
|
||||||
|
onRequestClose={handleCorrelationDisplayClose}
|
||||||
|
modalHeading={`Message ${messageInstanceForModal.id} (${messageInstanceForModal.name}) ${messageInstanceForModal.message_type} data:`}
|
||||||
|
modalLabel="Details"
|
||||||
|
>
|
||||||
|
{failureCausePre}
|
||||||
|
<p>Correlations:</p>
|
||||||
|
<pre>
|
||||||
|
{JSON.stringify(messageInstanceForModal.correlation_keys, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTable = () => {
|
||||||
|
const rows = messageInstances.map((row: MessageInstance) => {
|
||||||
|
let errorIcon = null;
|
||||||
|
let errorTitle = null;
|
||||||
|
if (row.failure_cause) {
|
||||||
|
errorTitle = 'Instance has an error';
|
||||||
|
errorIcon = (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<ErrorOutline className="red-icon" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let processLink = <span>External Call</span>;
|
||||||
|
let instanceLink = <span />;
|
||||||
|
if (row.process_instance_id != null) {
|
||||||
|
processLink = FormatProcessModelDisplayName(row);
|
||||||
|
instanceLink = (
|
||||||
|
<Link
|
||||||
|
data-qa="process-instance-show-link"
|
||||||
|
to={`/process-instances/${modifyProcessIdentifierForPathParam(
|
||||||
|
row.process_model_identifier,
|
||||||
|
)}/${row.process_instance_id}`}
|
||||||
|
>
|
||||||
|
{row.process_instance_id}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr key={row.id}>
|
||||||
|
<td>{row.id}</td>
|
||||||
|
<td>{processLink}</td>
|
||||||
|
<td>{instanceLink}</td>
|
||||||
|
<td>{row.name}</td>
|
||||||
|
<td>{row.message_type}</td>
|
||||||
|
<td>{row.counterpart_id}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
kind="ghost"
|
||||||
|
className="button-link"
|
||||||
|
onClick={() => setMessageInstanceForModal(row)}
|
||||||
|
title={errorTitle}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
{errorIcon}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td>{row.status}</td>
|
||||||
|
<td>
|
||||||
|
{DateAndTimeService.convertSecondsToFormattedDateTime(
|
||||||
|
row.created_at_in_seconds,
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Table striped bordered>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Process</th>
|
||||||
|
<th>Process instance</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Corresponding Message Instance</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created at</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pagination) {
|
||||||
|
const { page, perPage } = getPageInfoFromSearchParams(
|
||||||
|
searchParams,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
paginationQueryParamPrefix,
|
||||||
|
);
|
||||||
|
let breadcrumbElement = null;
|
||||||
|
if (searchParams.get('process_instance_id')) {
|
||||||
|
breadcrumbElement = (
|
||||||
|
<ProcessBreadcrumb
|
||||||
|
hotCrumbs={[
|
||||||
|
['Process Groups', '/process-groups'],
|
||||||
|
{
|
||||||
|
entityToExplode: searchParams.get('process_model_id') || '',
|
||||||
|
entityType: 'process-model-id',
|
||||||
|
linkLastItem: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
`Process Instance: ${searchParams.get('process_instance_id')}`,
|
||||||
|
`/process-instances/${searchParams.get(
|
||||||
|
'process_model_id',
|
||||||
|
)}/${searchParams.get('process_instance_id')}`,
|
||||||
|
],
|
||||||
|
['Messages'],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{breadcrumbElement}
|
||||||
|
{correlationsDisplayModal()}
|
||||||
|
<PaginationForTable
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
perPageOptions={[10, 50, 100, 500, 1000]}
|
||||||
|
pagination={pagination}
|
||||||
|
tableToDisplay={buildTable()}
|
||||||
|
paginationQueryParamPrefix={paginationQueryParamPrefix}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
330
spiffworkflow-frontend/src/a-spiffui-v3/helpers.tsx
Normal file
330
spiffworkflow-frontend/src/a-spiffui-v3/helpers.tsx
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { ElementForArray, ProcessInstance } from './interfaces';
|
||||||
|
|
||||||
|
export const DEFAULT_PER_PAGE = 50;
|
||||||
|
export const DEFAULT_PAGE = 1;
|
||||||
|
|
||||||
|
export const doNothing = () => {
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const matchNumberRegex = /^[0-9,.-]*$/;
|
||||||
|
|
||||||
|
// https://www.30secondsofcode.org/js/s/slugify
|
||||||
|
export const slugifyString = (str: string) => {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/[\s_-]+/g, '-')
|
||||||
|
.replace(/^-+/g, '')
|
||||||
|
.replace(/-+$/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HUMAN_TASK_TYPES = [
|
||||||
|
'User Task',
|
||||||
|
'Manual Task',
|
||||||
|
'UserTask',
|
||||||
|
'ManualTask',
|
||||||
|
'Task',
|
||||||
|
|
||||||
|
// HumanTaskModel.task_type is sometimes set to this and it is the same as "Task"
|
||||||
|
'NoneTask',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MULTI_INSTANCE_TASK_TYPES = [
|
||||||
|
'ParallelMultiInstanceTask',
|
||||||
|
'SequentialMultiInstanceTask',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LOOP_TASK_TYPES = ['StandardLoopTask'];
|
||||||
|
|
||||||
|
export const underscorizeString = (inputString: string) => {
|
||||||
|
return slugifyString(inputString).replace(/-/g, '_');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getKeyByValue = (
|
||||||
|
object: any,
|
||||||
|
value: string,
|
||||||
|
onAttribute?: string,
|
||||||
|
) => {
|
||||||
|
return Object.keys(object).find((key) => {
|
||||||
|
if (onAttribute) {
|
||||||
|
return object[key][onAttribute] === value;
|
||||||
|
}
|
||||||
|
return object[key] === value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
|
||||||
|
// so we convert undefined values to null recursively so that we can unset values in form fields
|
||||||
|
export const recursivelyChangeNullAndUndefined = (obj: any, newValue: any) => {
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((value: any, index: number) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
obj[index] = recursivelyChangeNullAndUndefined(value, newValue);
|
||||||
|
});
|
||||||
|
} else if (typeof obj === 'object') {
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
obj[key] = recursivelyChangeNullAndUndefined(value, newValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectKeysFromSearchParams = (obj: any, keys: string[]) => {
|
||||||
|
const newSearchParams: { [key: string]: string } = {};
|
||||||
|
keys.forEach((key: string) => {
|
||||||
|
const value = obj.get(key);
|
||||||
|
if (value) {
|
||||||
|
newSearchParams[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newSearchParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (string: any) => {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const titleizeString = (string: any) => {
|
||||||
|
return capitalizeFirstLetter((string || '').replaceAll('_', ' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const objectIsEmpty = (obj: object) => {
|
||||||
|
return Object.keys(obj).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPageInfoFromSearchParams = (
|
||||||
|
searchParams: any,
|
||||||
|
defaultPerPage: string | number = DEFAULT_PER_PAGE,
|
||||||
|
defaultPage: string | number = DEFAULT_PAGE,
|
||||||
|
paginationQueryParamPrefix: string | null = null,
|
||||||
|
) => {
|
||||||
|
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
|
||||||
|
? `${paginationQueryParamPrefix}_`
|
||||||
|
: '';
|
||||||
|
const page = parseInt(
|
||||||
|
searchParams.get(`${paginationQueryParamPrefixToUse}page`) ||
|
||||||
|
defaultPage.toString(),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const perPage = parseInt(
|
||||||
|
searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
|
||||||
|
defaultPerPage.toString(),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { page, perPage };
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/1349426/6090676
|
||||||
|
export const makeid = (length: number) => {
|
||||||
|
let result = '';
|
||||||
|
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for (let i = 0; i < length; i += 1) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProcessModelFullIdentifierFromSearchParams = (
|
||||||
|
searchParams: any,
|
||||||
|
) => {
|
||||||
|
let processModelFullIdentifier = null;
|
||||||
|
if (searchParams.get('process_model_identifier')) {
|
||||||
|
processModelFullIdentifier = `${searchParams.get(
|
||||||
|
'process_model_identifier',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
return processModelFullIdentifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/71352046/6090676
|
||||||
|
export const truncateString = (text: string, len: number) => {
|
||||||
|
if (text.length > len && text.length > 0) {
|
||||||
|
return `${text.split('').slice(0, len).join('')} ...`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pathFromFullUrl = (fullUrl: string) => {
|
||||||
|
const parsedURL = new URL(fullUrl);
|
||||||
|
return parsedURL.pathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Because of limitations in the way openapi defines parameters, we have to modify process models ids
|
||||||
|
// which are basically paths to the models
|
||||||
|
export const modifyProcessIdentifierForPathParam = (path: string) => {
|
||||||
|
return path.replace(/\//g, ':') || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unModifyProcessIdentifierForPathParam = (path: string) => {
|
||||||
|
return path.replace(/:/g, '/') || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGroupFromModifiedModelId = (modifiedId: string) => {
|
||||||
|
const finalSplitIndex = modifiedId.lastIndexOf(':');
|
||||||
|
return modifiedId.slice(0, finalSplitIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitProcessModelId = (processModelId: string) => {
|
||||||
|
return processModelId.split('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshAtInterval = (
|
||||||
|
interval: number,
|
||||||
|
timeout: number,
|
||||||
|
periodicFunction: Function,
|
||||||
|
cleanupFunction?: Function,
|
||||||
|
) => {
|
||||||
|
const intervalRef = setInterval(() => periodicFunction(), interval * 1000);
|
||||||
|
const timeoutRef = setTimeout(() => {
|
||||||
|
clearInterval(intervalRef);
|
||||||
|
if (cleanupFunction) {
|
||||||
|
cleanupFunction();
|
||||||
|
}
|
||||||
|
}, timeout * 1000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalRef);
|
||||||
|
if (cleanupFunction) {
|
||||||
|
cleanupFunction();
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutRef);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// bpmn:SubProcess shape elements do not have children
|
||||||
|
// but their moddle elements / businessOjects have flowElements
|
||||||
|
// that can include the moddleElement of the subprocesses
|
||||||
|
const getChildProcessesFromModdleElement = (bpmnModdleElement: any) => {
|
||||||
|
let elements: string[] = [];
|
||||||
|
bpmnModdleElement.flowElements.forEach((c: any) => {
|
||||||
|
if (c.$type === 'bpmn:SubProcess') {
|
||||||
|
elements.push(c.id);
|
||||||
|
elements = [...elements, ...getChildProcessesFromModdleElement(c)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChildProcesses = (bpmnElement: any) => {
|
||||||
|
let elements: string[] = [];
|
||||||
|
bpmnElement.children.forEach((c: any) => {
|
||||||
|
if (c.type === 'bpmn:Participant') {
|
||||||
|
if (c.businessObject.processRef) {
|
||||||
|
elements.push(c.businessObject.processRef.id);
|
||||||
|
}
|
||||||
|
elements = [...elements, ...getChildProcesses(c)];
|
||||||
|
} else if (c.type === 'bpmn:SubProcess') {
|
||||||
|
elements.push(c.id);
|
||||||
|
elements = [
|
||||||
|
...elements,
|
||||||
|
...getChildProcessesFromModdleElement(c.businessObject),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => {
|
||||||
|
const childProcesses = getChildProcesses(rootBpmnElement);
|
||||||
|
childProcesses.push(rootBpmnElement.businessObject.id);
|
||||||
|
return childProcesses;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPageTitle = (items: Array<string>) => {
|
||||||
|
document.title = ['SpiffWorkflow'].concat(items).join(' - ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// calling it isANumber to avoid confusion with other libraries
|
||||||
|
// that have isNumber methods
|
||||||
|
export const isANumber = (str: string | number | null) => {
|
||||||
|
if (str === undefined || str === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /^\d+(\.\d+)?$/.test(str.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodeBase64 = (data: string) => {
|
||||||
|
return Buffer.from(data).toString('base64');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeBase64 = (data: string) => {
|
||||||
|
return Buffer.from(data, 'base64').toString('ascii');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLastMilestoneFromProcessInstance = (
|
||||||
|
processInstance: ProcessInstance,
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
let valueToUse = value;
|
||||||
|
if (!valueToUse) {
|
||||||
|
if (processInstance.status === 'not_started') {
|
||||||
|
valueToUse = 'Created';
|
||||||
|
} else if (
|
||||||
|
['complete', 'error', 'terminated'].includes(processInstance.status)
|
||||||
|
) {
|
||||||
|
valueToUse = 'Completed';
|
||||||
|
} else {
|
||||||
|
valueToUse = 'Started';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let truncatedValue = valueToUse;
|
||||||
|
const milestoneLengthLimit = 20;
|
||||||
|
if (truncatedValue.length > milestoneLengthLimit) {
|
||||||
|
truncatedValue = `${truncatedValue.substring(
|
||||||
|
0,
|
||||||
|
milestoneLengthLimit - 3,
|
||||||
|
)}...`;
|
||||||
|
}
|
||||||
|
return [valueToUse, truncatedValue];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseTaskShowUrl = (url: string) => {
|
||||||
|
const path = pathFromFullUrl(url);
|
||||||
|
|
||||||
|
// expected url pattern:
|
||||||
|
// /tasks/[process_instance_id]/[task_guid]
|
||||||
|
return path.match(
|
||||||
|
/^\/tasks\/(\d+)\/([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})$/,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/15855457/6090676
|
||||||
|
export const isURL = (str: string) => {
|
||||||
|
const urlRegex =
|
||||||
|
// eslint-disable-next-line max-len, sonarjs/regex-complexity
|
||||||
|
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:[/?#]\S*)?$/i;
|
||||||
|
return urlRegex.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// this will help maintain order when using an array of elements.
|
||||||
|
// React needs to have a key for each component to ensure correct elements are added
|
||||||
|
// and removed when that array is changed.
|
||||||
|
// https://stackoverflow.com/questions/46735483/error-do-not-use-array-index-in-keys/46735689#46735689
|
||||||
|
export const renderElementsForArray = (elements: ElementForArray[]) => {
|
||||||
|
return elements.map((element: any) => (
|
||||||
|
<div key={element.key}>{element.component}</div>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertSvgElementToHtmlString = (svgElement: ReactElement) => {
|
||||||
|
// vite with svgr imports svg files as react components but we need the html string value at times
|
||||||
|
// so this converts the component to html string
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const root = createRoot(div);
|
||||||
|
flushSync(() => {
|
||||||
|
root.render(svgElement);
|
||||||
|
});
|
||||||
|
return div.innerHTML;
|
||||||
|
};
|
578
spiffworkflow-frontend/src/a-spiffui-v3/interfaces.ts
Normal file
578
spiffworkflow-frontend/src/a-spiffui-v3/interfaces.ts
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
import { ReactElement } from 'react';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAction {
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
export interface ApiActions {
|
||||||
|
[key: string]: ApiAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Secret {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
creator_user_id: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Onboarding {
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
process_instance_id?: string;
|
||||||
|
task_id?: string;
|
||||||
|
instructions: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessData {
|
||||||
|
process_data_identifier: string;
|
||||||
|
process_data_value: any;
|
||||||
|
|
||||||
|
authorized?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecentProcessModel {
|
||||||
|
processGroupIdentifier?: string;
|
||||||
|
processModelIdentifier: string;
|
||||||
|
processModelDisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskPropertiesJson {
|
||||||
|
parent: string;
|
||||||
|
last_state_change: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventDefinition {
|
||||||
|
typename: string;
|
||||||
|
payload: any;
|
||||||
|
event_definitions: [EventDefinition];
|
||||||
|
|
||||||
|
message_var?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskDefinitionPropertiesJson {
|
||||||
|
spec: string;
|
||||||
|
event_definition: EventDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignalButton {
|
||||||
|
label: string;
|
||||||
|
event: EventDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task withouth task data and form info - just the basics
|
||||||
|
export interface BasicTask {
|
||||||
|
id: number;
|
||||||
|
guid: string;
|
||||||
|
process_instance_id: number;
|
||||||
|
bpmn_identifier: string;
|
||||||
|
bpmn_name?: string;
|
||||||
|
bpmn_process_direct_parent_guid: string;
|
||||||
|
bpmn_process_definition_identifier: string;
|
||||||
|
state: string;
|
||||||
|
typename: string;
|
||||||
|
properties_json: TaskPropertiesJson;
|
||||||
|
task_definition_properties_json: TaskDefinitionPropertiesJson;
|
||||||
|
|
||||||
|
process_model_display_name: string;
|
||||||
|
process_model_identifier: string;
|
||||||
|
name_for_display: string;
|
||||||
|
can_complete: boolean;
|
||||||
|
|
||||||
|
start_in_seconds: number;
|
||||||
|
end_in_seconds: number;
|
||||||
|
extensions?: any;
|
||||||
|
|
||||||
|
process_model_uses_queued_execution?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: merge with ProcessInstanceTask
|
||||||
|
// Currently used like TaskModel in backend
|
||||||
|
export interface Task extends BasicTask {
|
||||||
|
data: any;
|
||||||
|
form_schema: any;
|
||||||
|
form_ui_schema: any;
|
||||||
|
signal_buttons: SignalButton[];
|
||||||
|
|
||||||
|
event_definition?: EventDefinition;
|
||||||
|
saved_form_data?: any;
|
||||||
|
runtime_info?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently used like ApiTask in backend
|
||||||
|
export interface ProcessInstanceTask {
|
||||||
|
id: string;
|
||||||
|
task_id: string;
|
||||||
|
can_complete: boolean;
|
||||||
|
created_at_in_seconds: number;
|
||||||
|
current_user_is_potential_owner: number;
|
||||||
|
data: any;
|
||||||
|
form_schema: any;
|
||||||
|
form_ui_schema: any;
|
||||||
|
lane_assignment_id: string;
|
||||||
|
last_milestone_bpmn_name: string;
|
||||||
|
name: string; // bpmn_identifier
|
||||||
|
process_identifier: string;
|
||||||
|
process_initiator_username: string;
|
||||||
|
process_instance_id: number;
|
||||||
|
process_instance_status: string;
|
||||||
|
process_instance_summary: string;
|
||||||
|
process_model_display_name: string;
|
||||||
|
process_model_identifier: string;
|
||||||
|
properties: any;
|
||||||
|
state: string;
|
||||||
|
title: string; // bpmn_name
|
||||||
|
type: string;
|
||||||
|
updated_at_in_seconds: number;
|
||||||
|
|
||||||
|
potential_owner_usernames?: string;
|
||||||
|
assigned_user_group_identifier?: string;
|
||||||
|
error_message?: string;
|
||||||
|
|
||||||
|
// these are actually from HumanTaskModel on the backend
|
||||||
|
task_title?: string;
|
||||||
|
task_name?: string;
|
||||||
|
completed?: boolean;
|
||||||
|
|
||||||
|
// gets shoved onto HumanTaskModel in result
|
||||||
|
completed_by_username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessReference {
|
||||||
|
identifier: string; // The unique id of the process
|
||||||
|
display_name: string;
|
||||||
|
relative_location: string;
|
||||||
|
type: string; // either "decision" or "process"
|
||||||
|
file_name: string;
|
||||||
|
properties: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ObjectWithStringKeysAndValues = { [key: string]: string };
|
||||||
|
|
||||||
|
export interface FilterOperator {
|
||||||
|
id: string;
|
||||||
|
requires_value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOperatorMapping {
|
||||||
|
[key: string]: FilterOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterDisplayTypeMapping {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessFile {
|
||||||
|
content_type: string;
|
||||||
|
last_modified: string;
|
||||||
|
name: string;
|
||||||
|
process_model_id: string;
|
||||||
|
references: ProcessReference[];
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
file_contents?: string;
|
||||||
|
file_contents_hash?: string;
|
||||||
|
bpmn_process_ids?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceMetadata {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstance {
|
||||||
|
id: number;
|
||||||
|
actions?: ApiActions;
|
||||||
|
bpmn_version_control_identifier: string;
|
||||||
|
bpmn_version_control_type: string;
|
||||||
|
bpmn_xml_file_contents?: string;
|
||||||
|
bpmn_xml_file_contents_retrieval_error?: string;
|
||||||
|
created_at_in_seconds: number;
|
||||||
|
end_in_seconds: number | null;
|
||||||
|
last_milestone_bpmn_name?: string;
|
||||||
|
process_initiator_username: string;
|
||||||
|
process_metadata?: ProcessInstanceMetadata[];
|
||||||
|
process_model_display_name: string;
|
||||||
|
process_model_identifier: string;
|
||||||
|
process_model_with_diagram_identifier?: string;
|
||||||
|
start_in_seconds: number | null;
|
||||||
|
status: string;
|
||||||
|
summary?: string;
|
||||||
|
updated_at_in_seconds: number;
|
||||||
|
|
||||||
|
// from tasks
|
||||||
|
assigned_user_group_identifier?: string;
|
||||||
|
potential_owner_usernames?: string;
|
||||||
|
task_id?: string;
|
||||||
|
task_name?: string;
|
||||||
|
task_title?: string;
|
||||||
|
task_updated_at_in_seconds?: number;
|
||||||
|
waiting_for?: string;
|
||||||
|
|
||||||
|
// from api instance
|
||||||
|
process_model_uses_queued_execution?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CorrelationProperty {
|
||||||
|
retrieval_expression: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CorrelationProperties {
|
||||||
|
[key: string]: CorrelationProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageDefinition {
|
||||||
|
correlation_properties: CorrelationProperties;
|
||||||
|
schema: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Messages {
|
||||||
|
[key: string]: MessageDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferenceCacheType = 'decision' | 'process' | 'data_store' | 'message';
|
||||||
|
|
||||||
|
export interface ReferenceCache {
|
||||||
|
identifier: string;
|
||||||
|
display_name: string;
|
||||||
|
relative_location: string;
|
||||||
|
type: ReferenceCacheType;
|
||||||
|
file_name: string;
|
||||||
|
properties: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageInstance {
|
||||||
|
correlation_keys: any;
|
||||||
|
counterpart_id: number;
|
||||||
|
created_at_in_seconds: number;
|
||||||
|
failure_cause: string;
|
||||||
|
id: number;
|
||||||
|
message_type: string;
|
||||||
|
name: string;
|
||||||
|
process_instance_id: number;
|
||||||
|
process_model_display_name: string;
|
||||||
|
process_model_identifier: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportFilter {
|
||||||
|
field_name: string;
|
||||||
|
// using any here so we can use this as a string and boolean
|
||||||
|
field_value: any;
|
||||||
|
operator?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportColumn {
|
||||||
|
Header: string;
|
||||||
|
accessor: string;
|
||||||
|
filterable: boolean;
|
||||||
|
display_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportColumnForEditing extends ReportColumn {
|
||||||
|
filter_field_value: string;
|
||||||
|
filter_operator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportMetadata {
|
||||||
|
columns: ReportColumn[];
|
||||||
|
filter_by: ReportFilter[];
|
||||||
|
order_by: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceReport {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
report_metadata: ReportMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessGroupLite {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
process_models?: ProcessModel[];
|
||||||
|
process_groups?: ProcessGroupLite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataExtractionPath {
|
||||||
|
key: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessModel {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
display_name: string;
|
||||||
|
primary_file_name: string;
|
||||||
|
primary_process_id: string;
|
||||||
|
files: ProcessFile[];
|
||||||
|
parent_groups?: ProcessGroupLite[];
|
||||||
|
metadata_extraction_paths?: MetadataExtractionPath[];
|
||||||
|
fault_or_suspend_on_exception?: string;
|
||||||
|
exception_notification_addresses?: string[];
|
||||||
|
bpmn_version_control_identifier?: string;
|
||||||
|
is_executable?: boolean;
|
||||||
|
actions?: ApiActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessGroup {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
description?: string | null;
|
||||||
|
process_models?: ProcessModel[];
|
||||||
|
process_groups?: ProcessGroup[];
|
||||||
|
parent_groups?: ProcessGroupLite[];
|
||||||
|
messages?: Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HotCrumbItemObject {
|
||||||
|
entityToExplode: ProcessModel | ProcessGroup | string;
|
||||||
|
entityType: string;
|
||||||
|
linkLastItem?: boolean;
|
||||||
|
checkPermission?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HotCrumbItemArray = [displayValue: string, url?: string];
|
||||||
|
|
||||||
|
// tuple of display value and URL
|
||||||
|
export type HotCrumbItem = HotCrumbItemArray | HotCrumbItemObject;
|
||||||
|
|
||||||
|
export interface ErrorForDisplay {
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
error_code?: string;
|
||||||
|
error_line?: string;
|
||||||
|
file_name?: string;
|
||||||
|
line_number?: number;
|
||||||
|
messageClassName?: string;
|
||||||
|
sentry_link?: string;
|
||||||
|
stacktrace?: string[];
|
||||||
|
task_id?: string;
|
||||||
|
task_name?: string;
|
||||||
|
task_trace?: string[];
|
||||||
|
|
||||||
|
task_type?: string;
|
||||||
|
output_data?: any;
|
||||||
|
expected_data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationParam {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationItem {
|
||||||
|
id: string;
|
||||||
|
parameters: AuthenticationParam[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationObject {
|
||||||
|
count: number;
|
||||||
|
total: number;
|
||||||
|
pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarbonComboBoxSelection {
|
||||||
|
selectedItem: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarbonComboBoxProcessSelection {
|
||||||
|
selectedItem: ProcessReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionsToCheck {
|
||||||
|
[key: string]: string[];
|
||||||
|
}
|
||||||
|
export interface PermissionVerbResults {
|
||||||
|
[key: string]: boolean;
|
||||||
|
}
|
||||||
|
export interface PermissionCheckResult {
|
||||||
|
[key: string]: PermissionVerbResults;
|
||||||
|
}
|
||||||
|
export interface PermissionCheckResponseBody {
|
||||||
|
results: PermissionCheckResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceEventErrorDetail {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
stacktrace: string[];
|
||||||
|
task_line_contents?: string;
|
||||||
|
task_line_number?: number;
|
||||||
|
task_offset?: number;
|
||||||
|
task_trace?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceLogEntry {
|
||||||
|
bpmn_process_definition_identifier: string;
|
||||||
|
bpmn_process_definition_name: string;
|
||||||
|
bpmn_task_type: string;
|
||||||
|
event_type: string;
|
||||||
|
spiff_task_guid: string;
|
||||||
|
task_definition_identifier: string;
|
||||||
|
task_guid: string;
|
||||||
|
timestamp: number;
|
||||||
|
id: number;
|
||||||
|
process_instance_id: number;
|
||||||
|
|
||||||
|
task_definition_name?: string;
|
||||||
|
user_id?: number;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessModelCaller {
|
||||||
|
display_name: string;
|
||||||
|
process_model_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserGroup {}
|
||||||
|
|
||||||
|
type InterstitialPageResponseType =
|
||||||
|
| 'task_update'
|
||||||
|
| 'error'
|
||||||
|
| 'unrunnable_instance';
|
||||||
|
|
||||||
|
export interface InterstitialPageResponse {
|
||||||
|
type: InterstitialPageResponseType;
|
||||||
|
error?: any;
|
||||||
|
task?: ProcessInstanceTask;
|
||||||
|
process_instance?: ProcessInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCaseErrorDetails {
|
||||||
|
error_messages: string[];
|
||||||
|
stacktrace?: string[];
|
||||||
|
task_bpmn_identifier?: string;
|
||||||
|
task_bpmn_name?: string;
|
||||||
|
task_line_contents?: string;
|
||||||
|
task_line_number?: number;
|
||||||
|
task_trace?: string[];
|
||||||
|
|
||||||
|
task_bpmn_type?: string;
|
||||||
|
output_data?: any;
|
||||||
|
expected_data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCaseResult {
|
||||||
|
bpmn_file: string;
|
||||||
|
passed: boolean;
|
||||||
|
test_case_identifier: string;
|
||||||
|
test_case_error_details?: TestCaseErrorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCaseResults {
|
||||||
|
all_passed: boolean;
|
||||||
|
failing: TestCaseResult[];
|
||||||
|
passing: TestCaseResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataStoreRecords {
|
||||||
|
results: any[];
|
||||||
|
pagination: PaginationObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataStore {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
schema: string;
|
||||||
|
description?: string | null;
|
||||||
|
location?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataStoreType {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonSchemaExample {
|
||||||
|
schema: any;
|
||||||
|
ui: any;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationOption {
|
||||||
|
identifier: string;
|
||||||
|
label: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskInstructionForEndUser {
|
||||||
|
task_guid: string;
|
||||||
|
process_instance_id: number;
|
||||||
|
instruction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessInstanceProgressResponse {
|
||||||
|
error_details?: ProcessInstanceEventErrorDetail;
|
||||||
|
instructions: TaskInstructionForEndUser[];
|
||||||
|
process_instance?: ProcessInstance;
|
||||||
|
process_instance_event?: ProcessInstanceLogEntry;
|
||||||
|
task?: ProcessInstanceTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
function: Function;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyboardShortcuts {
|
||||||
|
[key: string]: KeyboardShortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpiffTab {
|
||||||
|
path: string;
|
||||||
|
display_name: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpiffTableHeader {
|
||||||
|
tooltip_text?: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElementForArray {
|
||||||
|
key: string;
|
||||||
|
component: ReactElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicTaskForm {
|
||||||
|
form_schema: any;
|
||||||
|
form_ui_schema: any;
|
||||||
|
instructions_for_end_user?: string;
|
||||||
|
}
|
||||||
|
export interface PublicTask {
|
||||||
|
form: PublicTaskForm;
|
||||||
|
task_guid: string;
|
||||||
|
process_instance_id: number;
|
||||||
|
confirmation_message_markdown: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RJSFFormObject {
|
||||||
|
formData: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MigrationEvent {
|
||||||
|
id: number;
|
||||||
|
initial_bpmn_process_hash: string;
|
||||||
|
initial_git_revision: string;
|
||||||
|
target_bpmn_process_hash: string;
|
||||||
|
target_git_revision: string;
|
||||||
|
timestamp: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
export interface MigrationCheckResult {
|
||||||
|
can_migrate: boolean;
|
||||||
|
process_instance_id: number;
|
||||||
|
current_git_revision: string;
|
||||||
|
current_bpmn_process_hash: string;
|
||||||
|
}
|
@ -0,0 +1,275 @@
|
|||||||
|
import { Duration, format, parse } from 'date-fns';
|
||||||
|
import {
|
||||||
|
DATE_TIME_FORMAT,
|
||||||
|
DATE_FORMAT,
|
||||||
|
TIME_FORMAT_HOURS_MINUTES,
|
||||||
|
} from '../config';
|
||||||
|
import { isANumber } from '../helpers';
|
||||||
|
|
||||||
|
const MINUTES_IN_HOUR = 60;
|
||||||
|
const SECONDS_IN_MINUTE = 60;
|
||||||
|
const SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE;
|
||||||
|
const FOUR_HOURS_IN_SECONDS = SECONDS_IN_HOUR * 4;
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL_SECONDS = 5;
|
||||||
|
const REFRESH_TIMEOUT_SECONDS = FOUR_HOURS_IN_SECONDS;
|
||||||
|
|
||||||
|
const stringLooksLikeADate = (dateString: string): boolean => {
|
||||||
|
// We had been useing date-fns parse to really check if a date is valid however it attempts to parse dates like 14-06-2 because it thinks it is 14-06-0002.
|
||||||
|
// This results in a validate date but has a negative time with getTime which is a valid number however we do not want dates like this at all.
|
||||||
|
// Checking for negative numbers seem wrong so use a regex to see if it looks anything like a date.
|
||||||
|
return (
|
||||||
|
(dateString.match(/^\d{4}[-/.]\d{2}[-/.]\d{2}$/) ||
|
||||||
|
dateString.match(/^(\d{1,2}|\w+)[-/.](\d{1,2}|\w+)[-/.]\d{4}$/) ||
|
||||||
|
dateString.match(/^\w+ +\d+, +\d{4}$/) ||
|
||||||
|
dateString.match(/^\d+ +\w+ +\d{4}$/)) !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateToSeconds = (date: any, onChangeFunction: any = null) => {
|
||||||
|
let dateInSeconds = date;
|
||||||
|
if (date !== null) {
|
||||||
|
let dateInMilliseconds = date;
|
||||||
|
if (typeof date.getTime === 'function') {
|
||||||
|
dateInMilliseconds = date.getTime();
|
||||||
|
}
|
||||||
|
dateInSeconds = Math.floor(dateInMilliseconds / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChangeFunction) {
|
||||||
|
onChangeFunction(dateInSeconds);
|
||||||
|
} else {
|
||||||
|
return dateInSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateObjectToFormattedString = (dateObject: Date) => {
|
||||||
|
if (dateObject) {
|
||||||
|
return format(dateObject, DATE_FORMAT);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateStringToYMDFormat = (dateString: string) => {
|
||||||
|
if (dateString === undefined || dateString === null) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dateString.match(/^\d{4}-\d{2}-\d{2}$/) ||
|
||||||
|
!stringLooksLikeADate(dateString)
|
||||||
|
) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
const newDate = parse(dateString, DATE_FORMAT, new Date());
|
||||||
|
// getTime returns NaN if the date is invalid
|
||||||
|
if (Number.isNaN(newDate.getTime())) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
return format(newDate, 'yyyy-MM-dd');
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateAndTimeStringsToDate = (
|
||||||
|
dateString: string,
|
||||||
|
timeString: string,
|
||||||
|
) => {
|
||||||
|
if (dateString && timeString) {
|
||||||
|
return new Date(`${dateStringToYMDFormat(dateString)}T${timeString}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateAndTimeStringsToSeconds = (
|
||||||
|
dateString: string,
|
||||||
|
timeString: string,
|
||||||
|
) => {
|
||||||
|
const dateObject = convertDateAndTimeStringsToDate(dateString, timeString);
|
||||||
|
if (dateObject) {
|
||||||
|
return convertDateToSeconds(dateObject);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertStringToDate = (dateString: string) => {
|
||||||
|
return convertDateAndTimeStringsToDate(dateString, '00:10:00');
|
||||||
|
};
|
||||||
|
|
||||||
|
const ymdDateStringToConfiguredFormat = (dateString: string) => {
|
||||||
|
const dateObject = convertStringToDate(dateString);
|
||||||
|
if (dateObject) {
|
||||||
|
return convertDateObjectToFormattedString(dateObject);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertSecondsToDateObject = (seconds: number) => {
|
||||||
|
if (seconds) {
|
||||||
|
return new Date(seconds * 1000);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertSecondsToFormattedDateTime = (seconds: number) => {
|
||||||
|
const dateObject = convertSecondsToDateObject(seconds);
|
||||||
|
if (dateObject) {
|
||||||
|
return format(dateObject, DATE_TIME_FORMAT);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => {
|
||||||
|
if (dateObject) {
|
||||||
|
return format(dateObject, TIME_FORMAT_HOURS_MINUTES);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => {
|
||||||
|
const dateObject = convertSecondsToDateObject(seconds);
|
||||||
|
if (dateObject) {
|
||||||
|
return convertDateObjectToFormattedHoursMinutes(dateObject);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertSecondsToFormattedDateString = (seconds: number) => {
|
||||||
|
const dateObject = convertSecondsToDateObject(seconds);
|
||||||
|
if (dateObject) {
|
||||||
|
return convertDateObjectToFormattedString(dateObject);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateStringToSeconds = (dateString: string) => {
|
||||||
|
const dateObject = convertStringToDate(dateString);
|
||||||
|
if (dateObject) {
|
||||||
|
return convertDateToSeconds(dateObject);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// logic from https://stackoverflow.com/a/28510323/6090676
|
||||||
|
const secondsToDuration = (secNum: number) => {
|
||||||
|
const days = Math.floor(secNum / 86400);
|
||||||
|
const hours = Math.floor(secNum / 3600) % 24;
|
||||||
|
const minutes = Math.floor(secNum / 60) % 60;
|
||||||
|
const seconds = secNum % 60;
|
||||||
|
|
||||||
|
const duration: Duration = {
|
||||||
|
days,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
seconds,
|
||||||
|
};
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptToConvertUnknownDateStringFormatToKnownFormat = (
|
||||||
|
dateString: string,
|
||||||
|
targetDateFormat?: string,
|
||||||
|
) => {
|
||||||
|
let dateFormat = targetDateFormat;
|
||||||
|
if (!dateFormat) {
|
||||||
|
dateFormat = DATE_FORMAT;
|
||||||
|
}
|
||||||
|
let newDateString = dateString;
|
||||||
|
// if the date starts with 4 digits then assume in y-m-d format and avoid all of this
|
||||||
|
if (stringLooksLikeADate(dateString) && !dateString.match(/^\d{4}/)) {
|
||||||
|
// if the date format should contain month names or abbreviations but does not have letters
|
||||||
|
// then attempt to parse in the same format but with digit months instead of letters
|
||||||
|
|
||||||
|
if (!dateString.match(/[a-zA-Z]+/) && dateFormat.match(/MMM/)) {
|
||||||
|
const numericalDateFormat = dateFormat.replaceAll(/MMM*/g, 'MM');
|
||||||
|
const dateFormatRegex = new RegExp(
|
||||||
|
numericalDateFormat
|
||||||
|
.replace('dd', '\\d{2}')
|
||||||
|
.replace('MM', '\\d{2}')
|
||||||
|
.replace('yyyy', '\\d{4}'),
|
||||||
|
);
|
||||||
|
const normalizedDateString = dateString.replaceAll(/[.-/]+/g, '-');
|
||||||
|
if (normalizedDateString.match(dateFormatRegex)) {
|
||||||
|
const newDate = parse(
|
||||||
|
normalizedDateString,
|
||||||
|
numericalDateFormat,
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
newDateString = convertDateObjectToFormattedString(newDate) || '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// NOTE: do not run Date.parse with y-m-d formats since it returns dates in a different timezone from other formats
|
||||||
|
const newDate = new Date(Date.parse(`${dateString}`));
|
||||||
|
newDateString = convertDateObjectToFormattedString(newDate) || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newDateString;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDurationForDisplay = (value: any) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const duration = secondsToDuration(parseInt(value, 10));
|
||||||
|
const durationTimes = [];
|
||||||
|
if (duration.seconds !== undefined && duration.seconds > 0) {
|
||||||
|
durationTimes.unshift(`${duration.seconds}s`);
|
||||||
|
}
|
||||||
|
if (duration.minutes !== undefined && duration.minutes > 0) {
|
||||||
|
durationTimes.unshift(`${duration.minutes}m`);
|
||||||
|
}
|
||||||
|
if (duration.hours !== undefined && duration.hours > 0) {
|
||||||
|
durationTimes.unshift(`${duration.hours}h`);
|
||||||
|
}
|
||||||
|
if (duration.days !== undefined && duration.days > 0) {
|
||||||
|
durationTimes.unshift(`${duration.days}d`);
|
||||||
|
}
|
||||||
|
if (durationTimes.length < 1) {
|
||||||
|
durationTimes.push('0s');
|
||||||
|
}
|
||||||
|
return durationTimes.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (value: any) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let dateInSeconds = value;
|
||||||
|
if (!isANumber(value)) {
|
||||||
|
const timeArgs = value.split('T');
|
||||||
|
dateInSeconds = convertDateAndTimeStringsToSeconds(
|
||||||
|
timeArgs[0],
|
||||||
|
timeArgs[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (dateInSeconds) {
|
||||||
|
return convertSecondsToFormattedDateTime(dateInSeconds);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateAndTimeService = {
|
||||||
|
REFRESH_INTERVAL_SECONDS,
|
||||||
|
REFRESH_TIMEOUT_SECONDS,
|
||||||
|
|
||||||
|
attemptToConvertUnknownDateStringFormatToKnownFormat,
|
||||||
|
convertDateAndTimeStringsToDate,
|
||||||
|
convertDateAndTimeStringsToSeconds,
|
||||||
|
convertDateObjectToFormattedHoursMinutes,
|
||||||
|
convertDateObjectToFormattedString,
|
||||||
|
convertDateStringToSeconds,
|
||||||
|
convertDateToSeconds,
|
||||||
|
convertSecondsToDateObject,
|
||||||
|
convertSecondsToFormattedDateString,
|
||||||
|
convertSecondsToFormattedDateTime,
|
||||||
|
convertSecondsToFormattedTimeHoursMinutes,
|
||||||
|
convertStringToDate,
|
||||||
|
dateStringToYMDFormat,
|
||||||
|
formatDateTime,
|
||||||
|
formatDurationForDisplay,
|
||||||
|
secondsToDuration,
|
||||||
|
stringLooksLikeADate,
|
||||||
|
ymdDateStringToConfiguredFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateAndTimeService;
|
200
spiffworkflow-frontend/src/a-spiffui-v3/services/HttpService.ts
Normal file
200
spiffworkflow-frontend/src/a-spiffui-v3/services/HttpService.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { BACKEND_BASE_URL } from '../config';
|
||||||
|
import { objectIsEmpty } from '../helpers';
|
||||||
|
import UserService from './UserService';
|
||||||
|
|
||||||
|
const HttpMethods = {
|
||||||
|
GET: 'GET',
|
||||||
|
POST: 'POST',
|
||||||
|
DELETE: 'DELETE',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBasicHeaders = (): Record<string, string> => {
|
||||||
|
if (UserService.isLoggedIn()) {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${UserService.getAccessToken()}`,
|
||||||
|
'SpiffWorkflow-Authentication-Identifier':
|
||||||
|
UserService.getAuthenticationIdentifier() || 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
type backendCallProps = {
|
||||||
|
path: string;
|
||||||
|
successCallback: Function;
|
||||||
|
failureCallback?: Function;
|
||||||
|
onUnauthorized?: Function;
|
||||||
|
httpMethod?: string;
|
||||||
|
extraHeaders?: object;
|
||||||
|
postBody?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UnauthenticatedError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UnauthenticatedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnexpectedResponseError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UnexpectedResponseError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageForHttpError = (statusCode: number, statusText: string) => {
|
||||||
|
let errorMessage = `HTTP Error ${statusCode}`;
|
||||||
|
if (statusText) {
|
||||||
|
errorMessage += `: ${statusText}`;
|
||||||
|
} else {
|
||||||
|
let httpTextForCode = '';
|
||||||
|
switch (statusCode) {
|
||||||
|
case 400:
|
||||||
|
httpTextForCode = 'Bad Request';
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
httpTextForCode = 'Unauthorized';
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
httpTextForCode = 'Forbidden';
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
httpTextForCode = 'Not Found';
|
||||||
|
break;
|
||||||
|
case 413:
|
||||||
|
httpTextForCode = 'Payload Too Large';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
httpTextForCode = 'Internal Server Error';
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
httpTextForCode = 'Bad Gateway';
|
||||||
|
break;
|
||||||
|
case 503:
|
||||||
|
httpTextForCode = 'Service Unavailable';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (httpTextForCode) {
|
||||||
|
errorMessage += `: ${httpTextForCode}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeCallToBackend = ({
|
||||||
|
path,
|
||||||
|
successCallback,
|
||||||
|
failureCallback,
|
||||||
|
onUnauthorized,
|
||||||
|
httpMethod = 'GET',
|
||||||
|
extraHeaders = {},
|
||||||
|
postBody = {},
|
||||||
|
}: // eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
backendCallProps) => {
|
||||||
|
const headers = getBasicHeaders();
|
||||||
|
|
||||||
|
if (!objectIsEmpty(extraHeaders)) {
|
||||||
|
Object.assign(headers, extraHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpArgs = {};
|
||||||
|
|
||||||
|
if (postBody instanceof FormData) {
|
||||||
|
Object.assign(httpArgs, { body: postBody });
|
||||||
|
} else if (typeof postBody === 'object') {
|
||||||
|
if (!objectIsEmpty(postBody)) {
|
||||||
|
// NOTE: stringify strips out keys with value undefined
|
||||||
|
Object.assign(httpArgs, { body: JSON.stringify(postBody) });
|
||||||
|
Object.assign(headers, { 'Content-Type': 'application/json' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.assign(httpArgs, { body: postBody });
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(httpArgs, {
|
||||||
|
headers: new Headers(headers as any),
|
||||||
|
method: httpMethod,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedPath = path.replace(/^\/v1\.0/, '');
|
||||||
|
|
||||||
|
fetch(`${BACKEND_BASE_URL}${updatedPath}`, httpArgs)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new UnauthenticatedError('You must be authenticated to do this.');
|
||||||
|
}
|
||||||
|
return response.text().then((result: any) => {
|
||||||
|
return { response, text: result };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
.then((result: any) => {
|
||||||
|
let jsonResult = null;
|
||||||
|
try {
|
||||||
|
jsonResult = JSON.parse(result.text);
|
||||||
|
} catch (error) {
|
||||||
|
const httpStatusMesage = messageForHttpError(
|
||||||
|
result.response.status,
|
||||||
|
result.response.statusText,
|
||||||
|
);
|
||||||
|
const baseMessage = `Received unexpected response from server. ${httpStatusMesage}.`;
|
||||||
|
console.error(`${baseMessage} Body: ${result.text}`);
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
throw new UnexpectedResponseError(baseMessage);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (result.response.status === 403) {
|
||||||
|
if (onUnauthorized) {
|
||||||
|
onUnauthorized(jsonResult);
|
||||||
|
} else if (UserService.isPublicUser()) {
|
||||||
|
window.location.href = '/public/sign-out';
|
||||||
|
} else {
|
||||||
|
// Hopefully we can make this service a hook and use the error message context directly
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(jsonResult.message);
|
||||||
|
}
|
||||||
|
} else if (!result.response.ok) {
|
||||||
|
if (failureCallback) {
|
||||||
|
failureCallback(jsonResult);
|
||||||
|
} else {
|
||||||
|
let message = 'A server error occurred.';
|
||||||
|
if (jsonResult.message) {
|
||||||
|
message = jsonResult.message;
|
||||||
|
}
|
||||||
|
console.error(message);
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
successCallback(jsonResult);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.name !== 'UnauthenticatedError') {
|
||||||
|
if (failureCallback) {
|
||||||
|
failureCallback(error);
|
||||||
|
} else {
|
||||||
|
console.error(error.message);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!UserService.isLoggedIn() &&
|
||||||
|
window.location.pathname !== '/login'
|
||||||
|
) {
|
||||||
|
window.location.href = `/login?original_url=${UserService.getCurrentLocation()}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const HttpService = {
|
||||||
|
HttpMethods,
|
||||||
|
makeCallToBackend,
|
||||||
|
messageForHttpError,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HttpService;
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography, Tabs, Tab, Box } from '@mui/material';
|
||||||
|
import MessageInstanceList from '../components/messages/MessageInstanceList';
|
||||||
|
import { setPageTitle } from '../../helpers';
|
||||||
|
|
||||||
|
export default function MessageListPage() {
|
||||||
|
setPageTitle(['Messages']);
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(0);
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
Messages
|
||||||
|
</Typography>
|
||||||
|
<Tabs value={value} onChange={handleChange} aria-label="Message tabs">
|
||||||
|
<Tab label="Message Instances" />
|
||||||
|
<Tab label="Message Models" />
|
||||||
|
</Tabs>
|
||||||
|
<Box role="tabpanel" hidden={value !== 0}>
|
||||||
|
<MessageInstanceList />
|
||||||
|
</Box>
|
||||||
|
<Box role="tabpanel" hidden={value !== 1}>
|
||||||
|
{/* Placeholder for MessageModelList */}
|
||||||
|
<Typography variant="body1">Message Models content goes here.</Typography>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -28,6 +28,7 @@ import ErrorDisplay from '../components/ErrorDisplay';
|
|||||||
import About from '../a-spiffui-v3/views/About';
|
import About from '../a-spiffui-v3/views/About';
|
||||||
import useAPIError from '../hooks/UseApiError';
|
import useAPIError from '../hooks/UseApiError';
|
||||||
import ComingSoon from '../components/ComingSoon';
|
import ComingSoon from '../components/ComingSoon';
|
||||||
|
import MessageListPage from '../a-spiffui-v3/views/MessageListPage';
|
||||||
|
|
||||||
const fadeIn = 'fadeIn';
|
const fadeIn = 'fadeIn';
|
||||||
const fadeOutImmediate = 'fadeOutImmediate';
|
const fadeOutImmediate = 'fadeOutImmediate';
|
||||||
@ -247,6 +248,7 @@ export default function SpiffUIV3() {
|
|||||||
{/* path="process-instances/:process_model_id/:process_instance_id/progress" */}
|
{/* path="process-instances/:process_model_id/:process_instance_id/progress" */}
|
||||||
{/* element={<ProcessInstanceProgressPage variant="all" />} */}
|
{/* element={<ProcessInstanceProgressPage variant="all" />} */}
|
||||||
{/* /> */}
|
{/* /> */}
|
||||||
|
<Route path="/messages" element={<MessageListPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user