mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 06:38:24 +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 useAPIError from '../hooks/UseApiError';
|
||||
import ComingSoon from '../components/ComingSoon';
|
||||
import MessageListPage from '../a-spiffui-v3/views/MessageListPage';
|
||||
|
||||
const fadeIn = 'fadeIn';
|
||||
const fadeOutImmediate = 'fadeOutImmediate';
|
||||
@ -247,6 +248,7 @@ export default function SpiffUIV3() {
|
||||
{/* path="process-instances/:process_model_id/:process_instance_id/progress" */}
|
||||
{/* element={<ProcessInstanceProgressPage variant="all" />} */}
|
||||
{/* /> */}
|
||||
<Route path="/messages" element={<MessageListPage />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</Box>
|
||||
|
Loading…
x
Reference in New Issue
Block a user