diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.module.scss b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.module.scss new file mode 100644 index 000000000..2eaaaff5c --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.module.scss @@ -0,0 +1,17 @@ +.GridContainer { + display: grid; + grid-template-columns: repeat(1, 1fr); +} + +.GridCell { + padding: 10px 5px; + border-bottom: 1px solid #ddd; +} + +.GridCell:last-child { + border-bottom: none; +} + +.GridCell:hover { + background-color: rgba(93, 117, 136, 0.05); +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.tsx new file mode 100644 index 000000000..5646911ce --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-activity.tsx @@ -0,0 +1,72 @@ +import { ReactNode } from 'react'; +import { Col, Row, Tag, Typography } from 'antd'; +import { ClockCircleFilled } from '@ant-design/icons'; +import styles from './element-activity.module.scss'; +import { ElementLike } from 'diagram-js/lib/model/Types'; +import { ExtendedInstanceInfo } from '@/lib/data/instance'; + +type EntryTextProps = React.ComponentProps; +const TimetableEntryText = (props: EntryTextProps) => ( + +); + +const ClockSymbol = () => ( + +); + +export function ElementActivity({ + processId, + element, + instance, +}: { + processId: string; + element?: ElementLike; + instance?: ExtendedInstanceInfo; +}) { + const activityEntries: ReactNode[][] = []; + + const activityLog: [string, 'INFO' | 'WARN', string][] = [ + ['09:14:02', 'INFO', 'Process started by m.chen'], + ['09:15:02', 'INFO', "ZStep 'Receive Application' started"], + ['09:16:13', 'INFO', "Step 'Receive Application' completed"], + ['09:18:23', 'INFO', "Gateway 'Application complete?' yes"], + ['09:23:13', 'INFO', "Step 'Credit Check' started"], + ['09:19:35', 'WARN', 'Credit Bureau response delayed (retry 1/3)'], + ['09:25:54', 'INFO', "Step 'Credit Check' completed"], + ['09:35:23', 'INFO', "Step 'Manager Approval' started"], + ]; + + const tagStatus: Record<'INFO' | 'WARN', string> = { + INFO: 'processing', + WARN: 'warning', + }; + + return ( +
+ {activityLog.map((row, idx_row) => ( + + + {row[0]} + + + + {row[1]} + + + + {row[2]} + + + ))} +
+ ); +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.module.scss b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.module.scss new file mode 100644 index 000000000..0ccbc3d26 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.module.scss @@ -0,0 +1,12 @@ +.ElementText { + word-break: normal; +} + +.ElementKeyText { + color: gray; + font-size: 0.8em; +} + +.ElementValueText { + font-size: 0.9em; +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.tsx new file mode 100644 index 000000000..fc80bfea4 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.tsx @@ -0,0 +1,647 @@ +import { CSSProperties, ReactNode, useEffect, useState } from 'react'; +import { Checkbox, Divider, Space, Switch, Tag, Typography } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; +import { getTiming } from './instance-helpers'; +import { + getDefinitionsInfos, + getDefinitionsVersionInformation, + getElementById, + getMetaDataFromElement, + getPerformersFromElement, + toBpmnObject, +} from '@proceed/bpmn-helper'; +import { DataGrid } from './instance-info-panel'; +import { generateDateString, generateDurationString } from '@/lib/utils'; +import type { ElementLike } from 'diagram-js/lib/core/Types'; +import { ExtendedInstanceInfo } from '@/lib/data/instance'; +import styles from './element-details.module.scss'; +import { EntryText } from './entry-text'; +import { DefinitionsInfos } from '@proceed/bpmn-helper/src/getters'; +import { getProcessVersion } from '@/lib/data/processes'; +import dynamic from 'next/dynamic'; +import { getUserById } from '@/lib/data/users'; +import { asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { User } from '@/lib/data/user-schema'; + +import cn from 'classnames'; +const TextViewer = dynamic(() => import('@/components/text-viewer'), { ssr: false }); + +type PreviousVersion = + | { + name: string; + id: string; + createdOn: Date; + description: string; + processId: string; + versionBasedOn: string | null; + bpmnFilePath: string; + } + | null + | undefined; + +type VersionInfo = { + versionId?: string | undefined; + name?: string | undefined; + description?: string | undefined; + versionBasedOn?: string | undefined; + versionCreatedOn?: string | undefined; +}; + +type EntryTextProps = React.ComponentProps; +const EntryKeyText = (props: EntryTextProps) => ( + +); +const EntryValueText = (props: EntryTextProps) => { + return ; +}; + +const TechEntryKey = (props: EntryTextProps) => ( + + + + TECH + + +); + +const TechDetailsSwitch = ({ + techDetails, + setTechDetailsCb, +}: { + techDetails: boolean; + setTechDetailsCb: (checked: boolean) => void; +}) => { + const textColor = techDetails ? '#3e93de' : '#aaa'; + const baseStyle: CSSProperties = { + width: '100%', + justifyContent: 'space-between', + flexWrap: 'nowrap', + alignItems: 'start', + display: 'inline-flex', + gap: 10, + }; + return ( +
+ setTechDetailsCb(checked)} /> + + + Show technical details + + + + {techDetails ? 'IDs & system info shown' : 'IDs & system info hidden'} + + +
+ ); +}; + +export function ElementDetails({ + processId, + element, + version, + instance, +}: { + processId: string; + element: ElementLike; + version: { bpmn: string }; + instance?: { + engines: { + id: string; + online: boolean; + }[]; + } & ExtendedInstanceInfo; +}) { + const [techDetails, setTechDetails] = useState(false); + const [definitionsInfos, setDefinitionsInfos] = useState(); + const [definitionsVersionInfos, setDefinitionsVersionInfos] = useState(); + const [previousVersion, setPreviousVersion] = useState(undefined); + const [responsibleParty, setResponsibleParty] = useState(undefined); + const [performers, setPerformers] = useState(undefined); + const detailsEntries: ReactNode[][] = []; + + const isRootElement = element && element.type === 'bpmn:Process'; + const metaData = getMetaDataFromElement(element.businessObject); + const token = instance?.tokens.find((l) => l.currentFlowElementId == element.id); + const logInfo = instance?.log.find((logEntry) => logEntry.flowElementId === element.id); + + useEffect(() => { + // using version because it contains the parent object containing some more metadata + async function getBpmnObject() { + setPerformers(undefined); + setResponsibleParty(undefined); + const bpmnObj = await toBpmnObject(version.bpmn); + const defInfos = await getDefinitionsInfos(bpmnObj); + const defVersionInfos = await getDefinitionsVersionInformation(bpmnObj); + const previous = await getProcessVersion(processId, defVersionInfos.versionBasedOn); + + if (element) { + if (element.type === 'bpmn:Process') { + // maybe to be used in the future + // const responsible = getElementsByTagName(bpmnObj, 'proceed:ResponsibleParty'); + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(version.bpmn, 'text/xml'); + const processXML = xmlDoc.getElementById(element.id); + const responsibleIds = processXML + ? JSON.parse( + processXML.getElementsByTagName('proceed:responsibleParty')[0].childNodes[1] + .childNodes[1].textContent || '', + ) + : []; + const responsible = await asyncMap( + responsibleIds.user, + async (resId: string) => await getUserById(resId), + ); + setResponsibleParty(responsible); + } else { + const elementPerformers: { user: string[] } = getPerformersFromElement( + getElementById(bpmnObj, element.id), + ); + const performers = elementPerformers.user + ? await asyncMap( + elementPerformers.user, + async (perfId: string) => await getUserById(perfId), + ) + : undefined; + setPerformers(performers); + } + } + + setDefinitionsInfos(defInfos); + setDefinitionsVersionInfos(defVersionInfos); + setPreviousVersion(previous); + } + getBpmnObject(); + }, [processId, version, element]); + + // TECH DETAILS SWITCH + detailsEntries.push([ + , + ]); + // INSTANCE DATA + if (isRootElement) { + // GENERAL + detailsEntries.push([ + + GENERAL + , + ]); + detailsEntries.push([ + Name, + {definitionsInfos?.name}, + ]); + detailsEntries.push([ + Short Name, + + {definitionsInfos?.userDefinedId} + , + ]); + detailsEntries.push([ + Documentation, + +
+ +
, + ]); + detailsEntries.push([ + Process Managager, + + + {responsibleParty ? ( + responsibleParty.map((e) => + !e.isGuest ? ( + + {e.firstName + ' ' + e.lastName} + + ) : undefined, + ) + ) : ( + + )} + + , + ]); + if (techDetails) + detailsEntries.push([ + ID, + {processId}, + ]); + + // VERSION DATA + detailsEntries.push([ + + + VERSION + , + ]); + detailsEntries.push([ + Version Name, + + {definitionsVersionInfos?.name} + , + ]); + detailsEntries.push([ + What changed, + + {definitionsVersionInfos?.description} + , + ]); + detailsEntries.push([ + Created on, + + {definitionsVersionInfos?.versionCreatedOn} + , + ]); + detailsEntries.push([ + Based on, + {previousVersion?.name}, + ]); + if (techDetails) + detailsEntries.push([ + Based on ID, + + {definitionsVersionInfos?.versionBasedOn} + , + ]); + + // INITIATOR + detailsEntries.push([ + + + WHO STARTED IT + , + ]); + const initiator = instance?.processInstanceInitiator; + detailsEntries.push([ + Started by, + + {typeof initiator === 'object' ? initiator.fullName : initiator} + , + ]); + if (typeof initiator === 'object') { + if (techDetails) + detailsEntries.push([ + Username, + {initiator.username}, + ]); + if (techDetails) + detailsEntries.push([ + User ID, + {initiator.id}, + ]); + detailsEntries.push([ + Workspace, + + {instance?.spaceOfProcessInstanceInitiator?.name} + , + ]); + if (techDetails) + detailsEntries.push([ + Workspace ID, + + {instance?.spaceOfProcessInstanceInitiator?.id} + , + ]); + } + + // TIMING + const { + actual: { start, end, duration }, + plan: { duration: plannedDuration }, + } = getTiming({ + isRootElement, + metaData, + token, + logInfo, + instance, + }); + + detailsEntries.push([ + + + TIMING + , + ]); + if (techDetails) + detailsEntries.push([ + Run ID, + {instance?.processInstanceId}, + ]); + detailsEntries.push([ + Planned duration, + + {generateDurationString(plannedDuration)} + , + ]); + detailsEntries.push([ + Start Time, + + {start && generateDateString(start, true)} + , + ]); + detailsEntries.push([ + End Time, + + {end && generateDateString(end, true)} + , + ]); + detailsEntries.push([ + Time so far, + + {generateDurationString(duration)} + , + ]); + + // ENGINE + + if (techDetails) { + detailsEntries.push([ + + + WHERE IT RUNS + , + ]); + detailsEntries.push([ + Engine, + // TODO: + , + ]); + detailsEntries.push([ + Engine ID, + + {instance?.engines.map((e: any) => e.id)} + , + ]); + } + // EVENT DATA + } else { + // GENERAL + detailsEntries.push([ + + GENERAL + , + ]); + detailsEntries.push([ + Name, + {element.businessObject?.name}, + ]); + detailsEntries.push([ + Type, + {element.type.split(':')[1]}, + ]); + detailsEntries.push([ + Description, +
+ {element.businessObject?.documentation?.[0].text ? ( + + ) : ( + + )} +
, + ]); + detailsEntries.push([ + Comes after, + + {element.businessObject.incoming && ( + + {element.businessObject.incoming.map((e: any) => ( + + {e.sourceRef.$type.split(':')[1]} + + ))} + + )} + , + ]); + if (techDetails) { + detailsEntries.push([ + {'Step ID'}, + + {element.id} + , + ]); + detailsEntries.push([ + {'Previous step ID'}, + + {element.businessObject.incoming && ( + + {element.businessObject.incoming.map((e: any) => ( + + {e.sourceRef.id} + + ))} + + )} + , + ]); + } + + // PEOPLE + detailsEntries.push([ + + + PEOPLE + , + ]); + detailsEntries.push([ + Assigned to, + + { + // loading from log if possible to skip fetching data + logInfo ? ( + + {logInfo?.performers?.user?.map((e) => + !e.isGuest ? ( + + {e.fullName} + + ) : undefined, + )} + + ) : performers ? ( + !!performers?.length && ( + + {performers.map((e) => + !e.isGuest ? ( + + {e.firstName + ' ' + e.lastName} + + ) : undefined, + )} + + ) + ) : ( + + ) + } + , + ]); + + detailsEntries.push([ + Done Bye, + + {logInfo?.actualOwner?.map((e) => ( + + {e.fullName} + + ))} + , + ]); + + if (techDetails) { + detailsEntries.push([ + Username, + + {logInfo?.actualOwner?.map((e) => e.username).toString()} + , + ]); + detailsEntries.push([ + User ID, + + {logInfo?.actualOwner?.map((e) => e.id).toString()} + , + ]); + } + + // TIMING + const { + actual: { start, end, duration }, + plan: { duration: plannedDuration }, + } = getTiming({ + isRootElement, + metaData, + token, + logInfo, + instance, + }); + detailsEntries.push([ + + + TIMING + , + ]); + detailsEntries.push([ + Planned duration, + + {plannedDuration && generateDurationString(plannedDuration)} + , + ]); + detailsEntries.push([ + Start time, + + {start && generateDateString(start, true)} + , + ]); + detailsEntries.push([ + End time, + + {end && generateDateString(end, true)} + , + ]); + detailsEntries.push([ + Time so far, + + {duration && generateDurationString(duration)} + , + ]); + + // OTHER + detailsEntries.push([ + + + OTHER + , + ]); + } + + // Is External + if (!isRootElement) { + detailsEntries.push([ + Runs outside this system, + , + ]); + } + + // User task + // TODO: editable priority + if (element.type === 'bpmn:UserTask') { + let priority: number | undefined = undefined; + + if (instance) { + if (token) priority = token.priority; + else if (logInfo) priority = logInfo.priority; + } else { + priority = metaData['defaultPriority']; + } + + detailsEntries.push([ + Priority, + + {priority} + , + ]); + } + + return ( + <> + {/* */} + + + ); +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.module.scss b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.module.scss new file mode 100644 index 000000000..916cf1751 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.module.scss @@ -0,0 +1,36 @@ +.GridContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +.GridCell { + padding: 12; + border-right: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} +// removing right border for every second element +.GridCell:nth-child(2n) { + border-right: none; +} +// removing the last two bottom borders if number of elements +// is even otherwise remove only the bottom border of the last one +.GridContainer:has(> :last-child:nth-child(even)) + > .GridCell:nth-last-child(-n + 2) { + border-bottom: none; +} +.GridContainer:has(> :last-child:nth-child(odd)) > .GridCell:last-child { + border-bottom: none; +} +.GridCell:hover { + background-color: rgba(93, 117, 136, 0.05); +} + +.ListTitle { + font-weight: 600; + font-size: 0.9em; + color: gray; +} +.ListValue { + display: flex; + justify-content: right; +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.tsx new file mode 100644 index 000000000..e57c3362e --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.tsx @@ -0,0 +1,376 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { + Col, + Divider, + Image, + message, + Progress, + ProgressProps, + Row, + Space, + Typography, +} from 'antd'; +import { + getDefinitionsInfos, + getDefinitionsVersionInformation, + getMetaDataFromElement, + toBpmnObject, +} from '@proceed/bpmn-helper'; +import { getTiming, statusToType } from './instance-helpers'; +import { generateDateString, generateDurationString, generateNumberString } from '@/lib/utils'; +import styles from './element-overview.module.scss'; +import { EntryText } from './entry-text'; +import { ElementLike } from 'diagram-js/lib/model/Types'; +import { ExtendedInstanceInfo } from '@/lib/data/instance'; +import { DataGrid } from './instance-info-panel'; +import { DefinitionsInfos } from '@proceed/bpmn-helper/src/getters'; +import dynamic from 'next/dynamic'; +import { fallbackImage } from '@/components/image-upload'; +import { EntityType } from '@/lib/helpers/fileManagerHelpers'; +import { useFileManager } from '@/lib/useFileManager'; +const TextViewer = dynamic(() => import('@/components/text-viewer'), { ssr: false }); + +export function ElementOverview({ + processId, + element, + version, + instance, +}: { + processId: string; + element: ElementLike; + version: { bpmn: string }; + instance?: ExtendedInstanceInfo; +}) { + const [definitionsInfos, setDefinitionsInfos] = useState(); + + const [fileUrl, setFileUrl] = useState(); + const { download } = useFileManager({ + entityType: EntityType.PROCESS, + errorToasts: true, + dontUpdateProcessArtifactsReferences: true, + }); + + useEffect(() => { + async function getBpmnObject() { + const bpmnObj = await toBpmnObject(version.bpmn); + const defInfos = await getDefinitionsInfos(bpmnObj); + setDefinitionsInfos(defInfos); + } + async function downloadFile() { + // "loading" state + setFileUrl(undefined); + const metaData = getMetaDataFromElement(element.businessObject); + const fileName = metaData.overviewImage; + + if (fileName === undefined) { + return; + } + + if (fileName.startsWith('public/')) { + setFileUrl(fileName.replace('public/', '/')); + return; + } + + try { + const result = await download({ + entityId: processId, + filePath: fileName, + }); + if (!result.fileUrl) throw new Error('Response does not contain fileUrl'); + + setFileUrl(result.fileUrl); + } catch (error) { + console.error('Download failed:', error); + message.error('Failed to download image.'); + } + } + + downloadFile(); + getBpmnObject(); + }, [processId, version, element, download]); + + const overviewEntries: ReactNode[][] = []; + const metaData = getMetaDataFromElement(element.businessObject); + const isRootElement = element && element.type === 'bpmn:Process'; + const token = instance?.tokens.find((l) => l.currentFlowElementId == element.id); + const logInfo = instance?.log.find((logEntry) => logEntry.flowElementId === element.id); + const initiator = instance?.processInstanceInitiator; + + // Element image + if (metaData.overviewImage) { + overviewEntries.push([ +
+ Image +
, + ]); + } else { + overviewEntries.push([ +
+ Process/Step image +
, + ]); + } + + if (isRootElement) { + // Name and Shortname + overviewEntries.push([ +
+ + {definitionsInfos?.name} + + + {definitionsInfos?.userDefinedId} + +
, + ]); + + // description + overviewEntries.push([ +
+ +
, + ]); + } else { + // Name and Shortname + overviewEntries.push([ +
+ + {definitionsInfos?.name} + + + {definitionsInfos?.userDefinedId} + +
, + ]); + + // description + overviewEntries.push([ +
+ +
, + ]); + + // Element status + let status = undefined; + if (isRootElement && instance) { + status = instance.instanceState[0]; + } else if (element && instance) { + const elementInfo = instance.log.find((l) => l.flowElementId == element.id); + if (elementInfo) { + status = elementInfo.executionState; + } else { + const tokenInfo = instance.tokens.find((l) => l.currentFlowElementId == element.id); + status = tokenInfo ? tokenInfo.currentFlowNodeState : 'WAITING'; + } + } + + const statusType = status && statusToType(status); + // Progress + // TODO: editable progress + // see src/management-system/src/frontend/components/deployments/activityInfo/ProgressSetter.vue + if (instance && !isRootElement) { + let progress: + | { value: number; manual: boolean; milestoneCalculatedProgress?: number } + | undefined = undefined; + if (token && token.currentFlowNodeProgress) { + let milestoneCalculatedProgress = 0; + if (token.milestones && Object.keys(token.milestones).length > 0) { + const milestoneProgressValues = Object.values(token.milestones); + milestoneCalculatedProgress = + milestoneProgressValues.reduce((acc, milestoneVal) => acc + milestoneVal) / + milestoneProgressValues.length; + } + + progress = { + ...token.currentFlowNodeProgress, + milestoneCalculatedProgress, + }; + } else if (logInfo?.progress) { + progress = logInfo.progress; + } + + if (progress) { + let progressStatus: ProgressProps['status'] = 'normal'; + if (statusType === 'success') progressStatus = 'success'; + else if (statusType === 'error') progressStatus = 'exception'; + + overviewEntries.push([ + , + ]); + } + } + } + // time info + const { + actual: { start: startDate, duration }, + plan: { duration: plannedDuration }, + } = getTiming({ + isRootElement, + metaData, + token, + logInfo, + instance, + }); + // Timing + overviewEntries.push([ +
+
+
+ Started +
+ {generateDateString(startDate, true)} +
+ +
+ Running for +
+ {generateDurationString(duration)} +
+ +
+ Planned +
+ {generateDurationString(plannedDuration)} +
+ +
+ Started by +
+ {typeof initiator === 'object' ? initiator.fullName : initiator} +
+
+
, + ]); + + // Budget + overviewEntries.push([ + + BUDGET + , + ]); + + const costsPlanned = metaData.costsPlanned + ? generateNumberString(metaData.costsPlanned.value, { + style: 'currency', + currency: metaData.costsPlanned.unit, + }) || metaData.costsPlanned.value + ' ' + metaData.costsPlanned.unit + : undefined; + overviewEntries.push([ + + + + Planned + + + {costsPlanned} + + + {/* TODO add calculated */} + + + + Actual + + + {/* TODO: add override */} + {costsPlanned} + {/* + Override + */} + + + , + ]); + // ELEMENTS FOR CALCULATED VALUE + // + // + // + // Calculated + // + // + // {/* TODO: */} + // {costsPlanned} + // + // + + return ; +} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-status.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-status.tsx deleted file mode 100644 index 594253df0..000000000 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-status.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { ReactNode } from 'react'; -import { Alert, Checkbox, Image, Progress, ProgressProps, Space, Typography } from 'antd'; -import { ClockCircleFilled } from '@ant-design/icons'; -import { getTiming, statusToType } from './instance-helpers'; -import { getMetaDataFromElement } from '@proceed/bpmn-helper'; -import { DisplayTable } from './instance-info-panel'; -import endpointBuilder from '@/lib/engines/endpoints/endpoint-builder'; -import { generateDateString, generateDurationString, generateNumberString } from '@/lib/utils'; -import type { ElementLike } from 'diagram-js/lib/core/Types'; -import { ExtendedInstanceInfo } from '@/lib/data/instance'; - -export function ElementStatus({ - processId, - element, - instance, -}: { - processId: string; - element: ElementLike; - instance?: ExtendedInstanceInfo; -}) { - const statusEntries: ReactNode[][] = []; - - const isRootElement = element && element.type === 'bpmn:Process'; - const metaData = getMetaDataFromElement(element.businessObject); - const token = instance?.tokens.find((l) => l.currentFlowElementId == element.id); - const logInfo = instance?.log.find((logEntry) => logEntry.flowElementId === element.id); - - // Element image - if (metaData.overviewImage) - statusEntries.push([ - 'Image', -
- {/** TODO: correct image url */} - Image linked to the element -
, - ]); - - // Element status - let status = undefined; - if (isRootElement && instance) { - status = instance.instanceState[0]; - } else if (element && instance) { - const elementInfo = instance.log.find((l) => l.flowElementId == element.id); - if (elementInfo) { - status = elementInfo.executionState; - } else { - const tokenInfo = instance.tokens.find((l) => l.currentFlowElementId == element.id); - status = tokenInfo ? tokenInfo.currentFlowNodeState : 'WAITING'; - } - } - const statusType = status && statusToType(status); - - statusEntries.push([ - 'Current state:', - status && statusType && , - ]); - - // from ./src/management-system/src/frontend/components/deployments/activityInfo/ActivityStatusInformation.vue - // TODO: Editable state? - - // Is External - if (!isRootElement) { - statusEntries.push([ - 'External:', - , - ]); - } - - // Progress - // TODO: editable progress - // see src/management-system/src/frontend/components/deployments/activityInfo/ProgressSetter.vue - if (instance && !isRootElement) { - let progress: - | { value: number; manual: boolean; milestoneCalculatedProgress?: number } - | undefined = undefined; - if (token && token.currentFlowNodeProgress) { - let milestoneCalculatedProgress = 0; - if (token.milestones && Object.keys(token.milestones).length > 0) { - const milestoneProgressValues = Object.values(token.milestones); - milestoneCalculatedProgress = - milestoneProgressValues.reduce((acc, milestoneVal) => acc + milestoneVal) / - milestoneProgressValues.length; - } - - progress = { - ...token.currentFlowNodeProgress, - milestoneCalculatedProgress, - }; - } else if (logInfo?.progress) { - progress = logInfo.progress; - } - - if (progress) { - let progressStatus: ProgressProps['status'] = 'normal'; - if (statusType === 'success') progressStatus = 'success'; - else if (statusType === 'error') progressStatus = 'exception'; - - statusEntries.push([ - 'Progress', - , - ]); - } - } - - // User task - // TODO: editable priority - if (element.type === 'bpmn:UserTask') { - let priority: number | undefined = undefined; - - if (instance) { - if (token) priority = token.priority; - else if (logInfo) priority = logInfo.priority; - } else { - priority = metaData['defaultPriority']; - } - - statusEntries.push(['Priority:', priority]); - } - - // Planned costs - const costs: { value: string; unit: string } | undefined = metaData['costsPlanned']; - statusEntries.push([ - 'Planned Costs:', - costs && - generateNumberString(+costs.value, { - style: 'currency', - currency: costs.unit, - }), - ]); - - // Documentation - statusEntries.push(['Documentation:', element.businessObject?.documentation?.[0]?.text]); - - // Real Costs - // TODO: Set real costs - if (instance) { - let costs: string | undefined = undefined; - if (token) - costs = - token.costsRealSetByOwner && - generateNumberString(+token.costsRealSetByOwner.value, { - style: 'currency', - currency: token.costsRealSetByOwner.unit, - }); - else if (logInfo) - costs = - logInfo.costsRealSetByOwner && - generateNumberString(+logInfo.costsRealSetByOwner.value, { - style: 'currency', - currency: logInfo.costsRealSetByOwner.unit, - }); - else { - costs = instance.executionCosts - ?.map((c) => - generateNumberString(+c.value, { - style: 'currency', - currency: c.unit, - }), - ) - .join(' + '); - } - - statusEntries.push(['Real Costs:', costs]); - } - - const timing = getTiming({ - isRootElement, - metaData, - token, - logInfo, - instance, - }); - - // Activity time - statusEntries.push([ - - - Started: - {generateDateString(timing.actual.start, true)} - , - - - Planned Start: - {generateDateString(timing.plan.start, true) || ''} - , - - - Delay: - = 1000 ? 'danger' : undefined} - > - {generateDurationString(timing.delays.start)} - - , - ]); - - statusEntries.push([ - - - Duration: - {generateDurationString(timing.actual.duration)} - , - - - Planned Duration: - {generateDurationString(timing.plan.duration)} - , - - - Delay: - = 1000 ? 'danger' : undefined} - > - {generateDurationString(timing.delays.duration)} - - , - ]); - - statusEntries.push([ - - - Ended: - {generateDateString(timing.actual.end, true)} - , - - - Planned End: - {generateDateString(timing.plan.end, true) || ''} - , - - - Delay: - = 1000 ? 'danger' : undefined}> - {generateDurationString(timing.delays.end)} - - , - ]); - - return ; -} diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/entry-text.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/entry-text.tsx new file mode 100644 index 000000000..f3e867d8a --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/entry-text.tsx @@ -0,0 +1,29 @@ +import { Typography } from 'antd'; + +type EntryTextProps = React.ComponentProps & { + missingColorOverride?: string; + missingTextOverride?: string; +}; +/** + * component to display an antd Typography.Text component, + * that changes it's look if no children are passed. + * @param props of component + */ +export const EntryText = (props: EntryTextProps) => { + const { missingColorOverride, missingTextOverride, ...restProps } = props; + return restProps.children ? ( + + ) : ( + + {missingTextOverride ?? 'N/A'} + + ); +}; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-info-panel.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-info-panel.tsx index 8b6a10b9d..5a1e7d9f2 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-info-panel.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-info-panel.tsx @@ -1,31 +1,50 @@ import ResizableElement, { ResizableElementRefType } from '@/components/ResizableElement'; import CollapsibleCard from '@/components/collapsible-card'; import { ReactNode, useRef } from 'react'; -import { Drawer, Grid, Tabs } from 'antd'; +import { Button, Col, Drawer, Grid, Modal, Row, Tabs } from 'antd'; import type { ElementLike } from 'diagram-js/lib/core/Types'; -import { ElementStatus } from './element-status'; +import { ElementDetails } from './element-details'; import InstanceVariables from './instance-variables'; +import { ElementActivity } from './element-activity'; +import { ElementOverview } from './element-overview'; +import { StatusTag } from './status-tag'; import { ExtendedInstanceInfo } from '@/lib/data/instance'; -export function DisplayTable({ data }: { data: ReactNode[][] }) { - // TODO: make this responsive +export function DataGrid({ data }: { data: ReactNode[][] }) { return ( - - - {data.map((row, idx_row) => ( - - {row.map((cell, idx_cell) => ( - {row[0]} + ) : ( + <> + - {cell} - - ))} - - ))} - -
+ {data.map((row, idx_row) => ( + + {row.length == 1 ? ( +
+ {row[0]} + + + {row[1]} + + + )} + + ))} + ); } @@ -42,44 +61,55 @@ export default function InstanceInfoPanel({ open: boolean; processId: string; version: { bpmn: string }; - instance?: ExtendedInstanceInfo; + instance?: { + engines: { + id: string; + online: boolean; + }[]; + processInitiator: any; + spaceOfProcessInitiator: any; + } & ExtendedInstanceInfo; element?: ElementLike; refetch: () => void; }) { const resizableElementRef = useRef(null); - const breakpoints = Grid.useBreakpoint(); + const breakpoint = Grid.useBreakpoint(); const title = element?.businessObject?.name || element?.id || 'How to PROCEED?'; - if (breakpoints.xl && !open) return null; + if (breakpoint.xl && !open) return null; const tabs = element ? ( , - }, - { - key: 'Advanced', - label: 'Advanced', - children: 'How to proceed', - }, - { - key: 'Timing', - label: 'Timing', - children: 'How to proceed', + key: 'Overview', + label: 'Overview', + children: ( + + ), }, { - key: 'Assignments', - label: 'Assignments', - children: 'How to proceed', + key: 'Details', + label: 'Details', + children: ( + + ), }, { - key: 'Variables', - label: 'Variables', + key: 'Data', + label: 'Data', children: ( ), }, - { - key: 'Resources', - label: 'Resources', - children: 'How to proceed', - }, + // { + // key: 'Milestones', + // label: 'Milestones', + // children: 'How to proceed', + // }, + // { + // key: 'Activity', + // label: 'Activity', + // children: , + // }, ]} /> ) : null; - if (breakpoints.xl) - return ( - 38 px), Footer with 32px and Header with 64px, Padding of Toolbar 12px (=> Total 146px) - height: 'calc(100vh - 150px)', - }} - ref={resizableElementRef} - > - - {tabs} - - - ); + // TODO to be determined by higher forces + const hideFooter = true; - return ( - + return breakpoint.xl ? ( + 38 px), Footer with 32px and Header with 64px, Padding of Toolbar 12px (=> Total 146px) + height: 'calc(100vh - 150px)', + boxShadow: '0 3px 12px -4px rgba(0, 0, 0, 0.1), 0 6px 48px -2px rgba(0, 0, 0, 0.07)', + }} + ref={resizableElementRef} + > + + + {tabs} + + + ) : ( + + OK + + ) + } + > {tabs} - + ); } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-selector.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-selector.tsx new file mode 100644 index 000000000..8feea1857 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-selector.tsx @@ -0,0 +1,13 @@ +import { Menu, Typography } from 'antd'; +import React from 'react'; + +export const InstanceSelector = () => { + return ( + <> + + Please Select an instance. + + + + ); +}; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-variables.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-variables.tsx index 015f505b6..a5f39c6ee 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-variables.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-variables.tsx @@ -2,7 +2,20 @@ import React, { useState } from 'react'; import { EditOutlined } from '@ant-design/icons'; -import { App, Button, Form, Input, InputNumber, Modal, Switch, Table } from 'antd'; +import { + App, + Button, + Col, + Form, + Input, + InputNumber, + Modal, + Row, + Space, + Switch, + Table, + Typography, +} from 'antd'; import { updateVariables } from '@/lib/executions/instance-server-actions'; import { useEnvironment } from '@/components/auth-can'; import TextArea from 'antd/es/input/TextArea'; @@ -10,6 +23,7 @@ import { wrapServerCall } from '@/lib/wrap-server-call'; import useInstanceVariables, { Variable } from './use-instance-variables'; import { textFormatMap, typeLabelMap } from '@/lib/process-variable-schema'; import { ExtendedInstanceInfo } from '@/lib/data/instance'; +import { EntryText } from './entry-text'; type InstanceVariableProps = { processId: string; @@ -18,6 +32,11 @@ type InstanceVariableProps = { refetch: () => void; }; +type FieldTitleProps = React.ComponentProps; +const FormFieldTitle = (props: FieldTitleProps) => ( + +); + const InstanceVariables: React.FC = ({ processId, version, @@ -33,9 +52,22 @@ const InstanceVariables: React.FC = ({ const [form] = Form.useForm(); - const { variables } = useInstanceVariables({ version, instance }); + const { variables, variableDefinitions } = useInstanceVariables({ version, instance }); const [variableToEdit, setVariableToEdit] = useState(undefined); + const [variableDefinitionsToEdit, setvariableDefinitionsToEdit] = useState< + | { + name: string; + dataType: 'string' | 'number' | 'boolean' | 'object' | 'date' | 'array' | 'file'; + description?: string | undefined; + defaultValue?: string | undefined; + textFormat?: 'email' | 'url' | undefined; + requiredAtInstanceStartup?: boolean | undefined; + enum?: string | undefined; + const?: boolean | undefined; + } + | undefined + >(undefined); const columns: React.ComponentProps>['columns'] = [ { title: 'Name', dataIndex: 'name', key: 'name' }, @@ -74,6 +106,9 @@ const InstanceVariables: React.FC = ({ type="text" onClick={() => { setVariableToEdit(variable); + setvariableDefinitionsToEdit( + variableDefinitions.find((e) => e.name === variable?.name), + ); switch (variable.type) { case 'object': case 'array': @@ -117,6 +152,7 @@ const InstanceVariables: React.FC = ({ const handleClose = () => { setVariableToEdit(undefined); + setvariableDefinitionsToEdit(undefined); }; return ( @@ -134,6 +170,7 @@ const InstanceVariables: React.FC = ({ onCancel={handleClose} destroyOnHidden okButtonProps={{ loading: submitting }} + okText={'Save value'} onOk={async () => { await form.validateFields(); @@ -172,65 +209,156 @@ const InstanceVariables: React.FC = ({ }); }} > -
- + + Current Value + + + - {updatedValueInput} - - + ]} + > + {updatedValueInput} +
+ + +
+ + +
+ Type +
+ Text +
+ + +
+ Format +
+ + {variableDefinitionsToEdit?.dataType} + +
+ +
+ + +
+ Required at start +
+ + {variableDefinitionsToEdit?.requiredAtInstanceStartup ? 'Yes' : 'No'} + +
+ + +
+ Can be changed +
+ + {variableDefinitionsToEdit?.const ? 'No' : 'Yes'} + +
+ +
+ + +
+ Default value +
+ + {variableDefinitionsToEdit?.defaultValue} + +
+ + +
+ Allowed values +
+ {variableToEdit?.allowed} +
+ +
+ + + desc + + + + + + {variableDefinitionsToEdit?.description} + + + +
); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx index 541e02de0..145a153f7 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx @@ -289,103 +289,113 @@ export default function ProcessDeploymentView({ processId }: { processId: string }} > - - {/* Left group: Select Instance + Filter + Color */} - - setSelectedInstanceId(value)} + options={versionInstances?.map((instance, idx) => ({ + value: instance.processInstanceId, + label: `${idx + 1}. Instance: ${new Date(instance.globalStartTime).toLocaleString()}`, + }))} + placeholder="Select an instance" + /> + + {currentInstance?.offline && ( + + + } + size={40} + style={{ backgroundColor: 'inherit' }} + /> + + )} + + + ', + key: '-2', + }, + ] + : []), + ...(versions?.map((v) => ({ + key: v.id, + label: v.name, + disabled: false, + })) || []), + ], + selectable: true, + onSelect: ({ key }) => { + const versionId = key === '-2' ? undefined : key; + setSelectedVersionId(versionId); + + const instances = getVersionInstances(knownInstances, versionId); + if (!instances.some((i) => i.processInstanceId === selectedInstanceId)) { + const youngestInstance = getYoungestInstance(instances); + setSelectedInstanceId(youngestInstance?.processInstanceId); + } + }, + selectedKeys: selectedVersionId ? [selectedVersionId] : [], + }} + > + + - )} - - - + , + selectable: true, + onSelect: (item) => { + setSelectedColoring(item.key as ColorOptions); }, - ...(selectedVersion - ? [ - { - label: '', - key: '-2', - }, - ] - : []), - ...(versions?.map((v) => ({ - key: v.id, - label: v.name, - disabled: false, - })) || []), - ], - selectable: true, - onSelect: ({ key }) => { - const versionId = key === '-2' ? undefined : key; - setSelectedVersionId(versionId); - - const instances = getVersionInstances(knownInstances, versionId); - if (!instances.some((i) => i.processInstanceId === selectedInstanceId)) { - const youngestInstance = getYoungestInstance(instances); - setSelectedInstanceId(youngestInstance?.processInstanceId); - } - }, - selectedKeys: selectedVersionId ? [selectedVersionId] : [], - }} - > - - - - - - , - selectable: true, - onSelect: (item) => { - setSelectedColoring(item.key as ColorOptions); - }, - selectedKeys: [selectedColoring], - }} - > - + /> - + + )} + + + + + {enableInstanceCSVExport && ( + <> + + + + + + + + )} + {currentInstance && ( + + /> - - )} - {currentInstance && ( - + )} +