569 lines
17 KiB
TypeScript

import { useContext, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import {
Add,
Upload,
Download,
TrashCan,
Favorite,
Edit,
// @ts-ignore
} from '@carbon/icons-react';
import {
Accordion,
AccordionItem,
Button,
Grid,
Column,
Stack,
ButtonSet,
Modal,
FileUploader,
Table,
TableHead,
TableHeader,
TableRow,
TableCell,
TableBody,
// @ts-ignore
} from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
import {
PermissionsToCheck,
ProcessFile,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels'
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
export default function ProcessModelShow() {
const params = useParams();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processInstanceResult, setProcessInstanceResult] = useState(null);
const [reloadModel, setReloadModel] = useState<boolean>(false);
const [filesToUpload, setFilesToUpload] = useState<any>(null);
const [showFileUploadModal, setShowFileUploadModal] =
useState<boolean>(false);
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const modifiedProcessModelId = modifyProcessModelPath(
`${params.process_model_id}`
);
useEffect(() => {
const processResult = (result: ProcessModel) => {
setProcessModel(result);
setReloadModel(false);
storeRecentProcessModelInLocalStorage(result);
};
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`,
successCallback: processResult,
});
}, [reloadModel, modifiedProcessModelId]);
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
successCallback: setProcessInstanceResult,
failureCallback: setErrorMessage,
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
const processInstanceRunResultTag = () => {
if (processModel && processInstanceResult) {
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstanceResult as any).id;
const nextTask = (processInstanceResult as any).next_task;
if (nextTask && nextTask.state === 'READY') {
navigate(`/tasks/${processInstanceId}/${nextTask.id}`);
}
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstanceId} kicked off (
<Link
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
);
}
return null;
};
const onUploadedCallback = () => {
setReloadModel(true);
};
const reloadModelOhYeah = (_httpResult: any) => {
setReloadModel(!reloadModel);
};
// Remove this code from
const onDeleteFile = (fileName: string) => {
const url = `/process-models/${modifiedProcessModelId}/files/${fileName}`;
const httpMethod = 'DELETE';
HttpService.makeCallToBackend({
path: url,
successCallback: reloadModelOhYeah,
httpMethod,
});
};
const onSetPrimaryFile = (fileName: string) => {
const url = `/process-models/${modifiedProcessModelId}`;
const httpMethod = 'PUT';
const processModelToPass = {
primary_file_name: fileName,
};
HttpService.makeCallToBackend({
path: url,
successCallback: onUploadedCallback,
httpMethod,
postBody: processModelToPass,
});
};
const handleProcessModelFileResult = (processModelFile: ProcessFile) => {
if (
!('file_contents' in processModelFile) ||
processModelFile.file_contents === undefined
) {
setErrorMessage({
message: `Could not file file contents for file: ${processModelFile.name}`,
});
return;
}
let contentType = 'application/xml';
if (processModelFile.type === 'json') {
contentType = 'application/json';
}
const element = document.createElement('a');
const file = new Blob([processModelFile.file_contents], {
type: contentType,
});
const downloadFileName = processModelFile.name;
element.href = URL.createObjectURL(file);
element.download = downloadFileName;
document.body.appendChild(element);
element.click();
};
const downloadFile = (fileName: string) => {
setErrorMessage(null);
const processModelPath = `process-models/${modifiedProcessModelId}`;
HttpService.makeCallToBackend({
path: `/${processModelPath}/files/${fileName}`,
successCallback: handleProcessModelFileResult,
});
};
const profileModelFileEditUrl = (processModelFile: ProcessFile) => {
if (processModel) {
if (processModelFile.name.match(/\.(dmn|bpmn)$/)) {
return `/admin/process-models/${modifiedProcessModelId}/files/${processModelFile.name}`;
}
if (processModelFile.name.match(/\.(json|md)$/)) {
return `/admin/process-models/${modifiedProcessModelId}/form/${processModelFile.name}`;
}
}
return null;
};
const navigateToFileEdit = (processModelFile: ProcessFile) => {
const url = profileModelFileEditUrl(processModelFile);
if (url) {
navigate(url);
}
};
const renderButtonElements = (
processModelFile: ProcessFile,
isPrimaryBpmnFile: boolean
) => {
const elements = [];
elements.push(
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Edit}
iconDescription="Edit File"
hasIconOnly
size="lg"
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
onClick={() => navigateToFileEdit(processModelFile)}
/>
</Can>
);
elements.push(
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Download}
iconDescription="Download File"
hasIconOnly
size="lg"
onClick={() => downloadFile(processModelFile.name)}
/>
</Can>
);
elements.push(
<Can
I="DELETE"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete File"
hasIconOnly
description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
);
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push(
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
kind="ghost"
renderIcon={Favorite}
iconDescription="Set As Primary File"
hasIconOnly
size="lg"
onClick={() => onSetPrimaryFile(processModelFile.name)}
/>
</Can>
);
}
return elements;
};
const processModelFileList = () => {
if (!processModel) {
return null;
}
let constructedTag;
const tags = processModel.files.map((processModelFile: ProcessFile) => {
const isPrimaryBpmnFile =
processModelFile.name === processModel.primary_file_name;
let actionsTableCell = null;
if (processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
actionsTableCell = (
<TableCell key={`${processModelFile.name}-cell`}>
{renderButtonElements(processModelFile, isPrimaryBpmnFile)}
</TableCell>
);
}
let primarySuffix = '';
if (isPrimaryBpmnFile) {
primarySuffix = '- Primary File';
}
let fileLink = null;
const fileUrl = profileModelFileEditUrl(processModelFile);
if (fileUrl) {
if (ability.can('GET', targetUris.processModelFileCreatePath)) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
} else {
fileLink = <span>{processModelFile.name}</span>;
}
}
constructedTag = (
<TableRow key={processModelFile.name}>
<TableCell key={`${processModelFile.name}-cell`}>
{fileLink}
{primarySuffix}
</TableCell>
{actionsTableCell}
</TableRow>
);
return constructedTag;
});
// return <ul>{tags}</ul>;
const headers = ['Name', 'Actions'];
return (
<Table size="lg" useZebraStyles={false}>
<TableHead>
<TableRow>
{headers.map((header) => (
<TableHeader id={header} key={header}>
{header}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>{tags}</TableBody>
</Table>
);
};
const handleFileUploadCancel = () => {
setShowFileUploadModal(false);
};
const handleFileUpload = (event: any) => {
if (processModel) {
event.preventDefault();
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', filesToUpload[0]);
formData.append('fileName', filesToUpload[0].name);
HttpService.makeCallToBackend({
path: url,
successCallback: onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
});
}
setShowFileUploadModal(false);
};
const fileUploadModal = () => {
return (
<Modal
data-qa="modal-upload-file-dialog"
open={showFileUploadModal}
modalHeading="Upload File"
primaryButtonText="Upload"
secondaryButtonText="Cancel"
onSecondarySubmit={handleFileUploadCancel}
onRequestClose={handleFileUploadCancel}
onRequestSubmit={handleFileUpload}
>
<FileUploader
labelTitle="Upload files"
labelDescription="Max file size is 500mb. Only .bpmn, .dmn, and .json files are supported."
buttonLabel="Add file"
buttonKind="primary"
size="md"
filenameStatus="edit"
role="button"
accept={['.bpmn', '.dmn', '.json']}
disabled={false}
iconDescription="Delete file"
name=""
multiple={false}
onChange={(event: any) => setFilesToUpload(event.target.files)}
/>
</Modal>
);
};
const processModelButtons = () => {
if (!processModel) {
return null;
}
return (
<Grid condensed fullWidth>
<Column md={5} lg={9} sm={3}>
<Accordion align="end" open>
<AccordionItem
open
data-qa="files-accordion"
title={
<Stack orientation="horizontal">
<span>
<Button size="sm" kind="ghost">
Files
</Button>
</span>
</Stack>
}
>
<Can
I="POST"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonSet>
<Button
renderIcon={Upload}
data-qa="upload-file-button"
onClick={() => setShowFileUploadModal(true)}
size="sm"
kind=""
className="button-white-background"
>
Upload File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
size="sm"
>
New BPMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
size="sm"
>
New DMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
size="sm"
>
New JSON File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
size="sm"
>
New Markdown File
</Button>
</ButtonSet>
<br />
</Can>
{processModelFileList()}
</AccordionItem>
</Accordion>
</Column>
</Grid>
);
};
if (processModel) {
return (
<>
{fileUploadModal()}
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}`,
],
]}
/>
<h1>Process Model: {processModel.display_name}</h1>
<p className="process-description">{processModel.description}</p>
<Stack orientation="horizontal" gap={3}>
<Can
I="POST"
a={targetUris.processInstanceActionPath}
ability={ability}
>
<Button onClick={processInstanceCreateAndRun} variant="primary">
Run
</Button>
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
>
Edit process model
</Button>
</Can>
</Stack>
<br />
<br />
{processInstanceRunResultTag()}
<br />
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]}
/>
<br />
</Can>
{processModelButtons()}
</>
);
}
return null;
}