mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-01-18 06:01:20 +00:00
690 lines
20 KiB
TypeScript
690 lines
20 KiB
TypeScript
import { useContext, useEffect, useState } from 'react';
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
|
import {
|
|
Add,
|
|
Upload,
|
|
Download,
|
|
TrashCan,
|
|
Favorite,
|
|
Edit,
|
|
View,
|
|
ArrowRight,
|
|
// @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 {
|
|
getGroupFromModifiedModelId,
|
|
modifyProcessIdentifierForPathParam,
|
|
} from '../helpers';
|
|
import {
|
|
PermissionsToCheck,
|
|
ProcessFile,
|
|
ProcessInstance,
|
|
ProcessModel,
|
|
} from '../interfaces';
|
|
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
|
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
|
import { usePermissionFetcher } from '../hooks/PermissionService';
|
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
|
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
|
import { Notification } from '../components/Notification';
|
|
|
|
export default function ProcessModelShow() {
|
|
const params = useParams();
|
|
const setErrorMessage = (useContext as any)(ErrorContext)[1];
|
|
|
|
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
|
const [processInstance, setProcessInstance] =
|
|
useState<ProcessInstance | null>(null);
|
|
const [reloadModel, setReloadModel] = useState<boolean>(false);
|
|
const [filesToUpload, setFilesToUpload] = useState<any>(null);
|
|
const [showFileUploadModal, setShowFileUploadModal] =
|
|
useState<boolean>(false);
|
|
const [processModelPublished, setProcessModelPublished] = useState<any>(null);
|
|
const [publishDisabled, setPublishDisabled] = useState<boolean>(false);
|
|
const navigate = useNavigate();
|
|
|
|
const { targetUris } = useUriListForPermissions();
|
|
const permissionRequestData: PermissionsToCheck = {
|
|
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
|
|
[targetUris.processModelPublishPath]: ['POST'],
|
|
[targetUris.processInstanceListPath]: ['GET'],
|
|
[targetUris.processInstanceCreatePath]: ['POST'],
|
|
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
|
|
};
|
|
const { ability, permissionsLoaded } = usePermissionFetcher(
|
|
permissionRequestData
|
|
);
|
|
|
|
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
|
`${params.process_model_id}`
|
|
);
|
|
|
|
useEffect(() => {
|
|
const processResult = (result: ProcessModel) => {
|
|
setProcessModel(result);
|
|
setReloadModel(false);
|
|
};
|
|
HttpService.makeCallToBackend({
|
|
path: `/process-models/${modifiedProcessModelId}`,
|
|
successCallback: processResult,
|
|
});
|
|
}, [reloadModel, modifiedProcessModelId]);
|
|
|
|
const processInstanceRunResultTag = () => {
|
|
if (processInstance) {
|
|
return (
|
|
<Notification
|
|
title="Process Instance Kicked Off:"
|
|
onClose={() => setProcessInstance(null)}
|
|
>
|
|
<Link
|
|
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
|
|
data-qa="process-instance-show-link"
|
|
>
|
|
view
|
|
</Link>
|
|
</Notification>
|
|
);
|
|
}
|
|
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 navigateToProcessModels = (_result: any) => {
|
|
navigate(
|
|
`/admin/process-groups/${getGroupFromModifiedModelId(
|
|
modifiedProcessModelId
|
|
)}`
|
|
);
|
|
};
|
|
|
|
const deleteProcessModel = () => {
|
|
HttpService.makeCallToBackend({
|
|
path: `/process-models/${modifiedProcessModelId}`,
|
|
successCallback: navigateToProcessModels,
|
|
httpMethod: 'DELETE',
|
|
});
|
|
};
|
|
|
|
const postPublish = (value: any) => {
|
|
setPublishDisabled(false);
|
|
setProcessModelPublished(value);
|
|
};
|
|
|
|
const publishProcessModel = () => {
|
|
setPublishDisabled(true);
|
|
setProcessModelPublished(null);
|
|
HttpService.makeCallToBackend({
|
|
path: `/process-models/${modifiedProcessModelId}/publish`,
|
|
successCallback: postPublish,
|
|
httpMethod: 'POST',
|
|
});
|
|
};
|
|
|
|
const navigateToFileEdit = (processModelFile: ProcessFile) => {
|
|
const url = profileModelFileEditUrl(processModelFile);
|
|
if (url) {
|
|
navigate(url);
|
|
}
|
|
};
|
|
|
|
const renderButtonElements = (
|
|
processModelFile: ProcessFile,
|
|
isPrimaryBpmnFile: boolean
|
|
) => {
|
|
const elements = [];
|
|
let icon = View;
|
|
let actionWord = 'View';
|
|
if (ability.can('PUT', targetUris.processModelFileCreatePath)) {
|
|
icon = Edit;
|
|
actionWord = 'Edit';
|
|
}
|
|
elements.push(
|
|
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
|
<Button
|
|
kind="ghost"
|
|
renderIcon={icon}
|
|
iconDescription={`${actionWord} 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>
|
|
);
|
|
|
|
if (!isPrimaryBpmnFile) {
|
|
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 || !permissionsLoaded) {
|
|
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;
|
|
});
|
|
|
|
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 [fileUploadEvent, setFileUploadEvent] = useState(null);
|
|
const [duplicateFilename, setDuplicateFilename] = useState<String>('');
|
|
const [showOverwriteConfirmationPrompt, setShowOverwriteConfirmationPrompt] =
|
|
useState(false);
|
|
|
|
const doFileUpload = (event: any) => {
|
|
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,
|
|
});
|
|
setFilesToUpload(null);
|
|
};
|
|
|
|
const handleFileUploadCancel = () => {
|
|
setShowFileUploadModal(false);
|
|
setFilesToUpload(null);
|
|
};
|
|
const handleOverwriteFileConfirm = () => {
|
|
setShowOverwriteConfirmationPrompt(false);
|
|
doFileUpload(fileUploadEvent);
|
|
};
|
|
const handleOverwriteFileCancel = () => {
|
|
setShowOverwriteConfirmationPrompt(false);
|
|
setFilesToUpload(null);
|
|
};
|
|
|
|
const confirmOverwriteFileDialog = () => {
|
|
return (
|
|
<Modal
|
|
danger
|
|
open={showOverwriteConfirmationPrompt}
|
|
data-qa="file-overwrite-modal-confirmation-dialog"
|
|
modalHeading={`Overwrite the file: ${duplicateFilename}`}
|
|
modalLabel="Overwrite file?"
|
|
primaryButtonText="Yes"
|
|
secondaryButtonText="Cancel"
|
|
onSecondarySubmit={handleOverwriteFileCancel}
|
|
onRequestSubmit={handleOverwriteFileConfirm}
|
|
onRequestClose={handleOverwriteFileCancel}
|
|
/>
|
|
);
|
|
};
|
|
const displayOverwriteConfirmation = (filename: String) => {
|
|
setDuplicateFilename(filename);
|
|
setShowOverwriteConfirmationPrompt(true);
|
|
};
|
|
|
|
const checkDuplicateFile = (event: any) => {
|
|
if (processModel && processModel.files.length > 0) {
|
|
let foundExistingFile = false;
|
|
processModel.files.forEach((file) => {
|
|
if (file.name === filesToUpload[0].name) {
|
|
foundExistingFile = true;
|
|
}
|
|
});
|
|
if (foundExistingFile) {
|
|
displayOverwriteConfirmation(filesToUpload[0].name);
|
|
setFileUploadEvent(event);
|
|
} else {
|
|
doFileUpload(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleFileUpload = (event: any) => {
|
|
if (processModel) {
|
|
checkDuplicateFile(event);
|
|
}
|
|
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}
|
|
onDelete={() => setFilesToUpload(null)}
|
|
onChange={(event: any) => setFilesToUpload(event.target.files)}
|
|
/>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
const processModelFilesSection = () => {
|
|
if (!processModel) {
|
|
return null;
|
|
}
|
|
return (
|
|
<Grid
|
|
condensed
|
|
fullWidth
|
|
className="megacondensed process-model-files-section"
|
|
>
|
|
<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>
|
|
);
|
|
};
|
|
|
|
const processInstanceListTableButton = () => {
|
|
if (processModel) {
|
|
return (
|
|
<Grid fullWidth condensed>
|
|
<Column sm={{ span: 3 }} md={{ span: 4 }} lg={{ span: 3 }}>
|
|
<h2>Process Instances</h2>
|
|
</Column>
|
|
<Column
|
|
sm={{ span: 1, offset: 3 }}
|
|
md={{ span: 1, offset: 7 }}
|
|
lg={{ span: 1, offset: 15 }}
|
|
>
|
|
<Button
|
|
data-qa="process-instance-list-link"
|
|
kind="ghost"
|
|
renderIcon={ArrowRight}
|
|
iconDescription="Go to Filterable List"
|
|
hasIconOnly
|
|
size="lg"
|
|
onClick={() =>
|
|
navigate(
|
|
`/admin/process-instances?process_model_identifier=${processModel.id}`
|
|
)
|
|
}
|
|
/>
|
|
</Column>
|
|
</Grid>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const processModelPublishMessage = () => {
|
|
if (processModelPublished) {
|
|
const prUrl: string = processModelPublished.pr_url;
|
|
return (
|
|
<Notification
|
|
title="Model Published:"
|
|
onClose={() => setProcessModelPublished(false)}
|
|
>
|
|
<a href={prUrl} target="_void()">
|
|
View the changes and create a Pull Request
|
|
</a>
|
|
</Notification>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
if (processModel) {
|
|
return (
|
|
<>
|
|
{fileUploadModal()}
|
|
{confirmOverwriteFileDialog()}
|
|
<ProcessBreadcrumb
|
|
hotCrumbs={[
|
|
['Process Groups', '/admin'],
|
|
{
|
|
entityToExplode: processModel,
|
|
entityType: 'process-model',
|
|
},
|
|
]}
|
|
/>
|
|
{processModelPublishMessage()}
|
|
{processInstanceRunResultTag()}
|
|
<Stack orientation="horizontal" gap={1}>
|
|
<h1 className="with-icons">
|
|
Process Model: {processModel.display_name}
|
|
</h1>
|
|
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
|
|
<Button
|
|
kind="ghost"
|
|
data-qa="edit-process-model-button"
|
|
renderIcon={Edit}
|
|
iconDescription="Edit Process Model"
|
|
hasIconOnly
|
|
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
|
>
|
|
Edit process model
|
|
</Button>
|
|
</Can>
|
|
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
|
|
<ButtonWithConfirmation
|
|
kind="ghost"
|
|
data-qa="delete-process-model-button"
|
|
renderIcon={TrashCan}
|
|
iconDescription="Delete Process Model"
|
|
hasIconOnly
|
|
description={`Delete process model: ${processModel.display_name}`}
|
|
onConfirmation={deleteProcessModel}
|
|
confirmButtonLabel="Delete"
|
|
/>
|
|
</Can>
|
|
</Stack>
|
|
<p className="process-description">{processModel.description}</p>
|
|
<Stack orientation="horizontal" gap={3}>
|
|
<Can
|
|
I="POST"
|
|
a={targetUris.processInstanceCreatePath}
|
|
ability={ability}
|
|
>
|
|
<>
|
|
<ProcessInstanceRun
|
|
processModel={processModel}
|
|
onSuccessCallback={setProcessInstance}
|
|
/>
|
|
<br />
|
|
<br />
|
|
</>
|
|
</Can>
|
|
<Can
|
|
I="POST"
|
|
a={targetUris.processModelPublishPath}
|
|
ability={ability}
|
|
>
|
|
<Button disabled={publishDisabled} onClick={publishProcessModel}>
|
|
Publish Changes
|
|
</Button>
|
|
</Can>
|
|
</Stack>
|
|
{processModelFilesSection()}
|
|
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
|
|
{processInstanceListTableButton()}
|
|
<ProcessInstanceListTable
|
|
filtersEnabled={false}
|
|
processModelFullIdentifier={processModel.id}
|
|
perPageOptions={[2, 5, 25]}
|
|
showReports={false}
|
|
/>
|
|
</Can>
|
|
</>
|
|
);
|
|
}
|
|
return null;
|
|
}
|