diff --git a/src/helper-modules/bpmn-helper/index.d.ts b/src/helper-modules/bpmn-helper/index.d.ts index 1b53eb259..71e84c207 100644 --- a/src/helper-modules/bpmn-helper/index.d.ts +++ b/src/helper-modules/bpmn-helper/index.d.ts @@ -285,8 +285,28 @@ declare const _exports: { area: getters.AreaInfos[]; workingPlace: getters.WorkingPlaceInfos[]; }; - getPerformersFromElement(element: object): any[]; - getPerformersFromElementById(bpmn: string | object, elementId: string): any[]; + getPerformersFromElement(element: object): { + user: string[]; + roles: string[]; + }; + getPerformersFromElementById( + bpmn: string | object, + elementId: string, + ): { + user: string[]; + roles: string[]; + }; + getResponsiblePartyFromElement(element: object): { + user: string[]; + roles: string[]; + }; + getResponsiblePartyFromElementId( + bpmn: string | object, + elementId: string, + ): { + user: string[]; + roles: string[]; + }; getPotentialOwnersFromElementById( elementId: string, bpmn: string | object, diff --git a/src/helper-modules/bpmn-helper/src/getters.d.ts b/src/helper-modules/bpmn-helper/src/getters.d.ts index 470aaed58..94c47f244 100644 --- a/src/helper-modules/bpmn-helper/src/getters.d.ts +++ b/src/helper-modules/bpmn-helper/src/getters.d.ts @@ -747,17 +747,58 @@ export function getLocationsFromElement(element: object): { * Get the performers for given element * * @param {object} element - * @returns {Array} performers given for element + * @returns {{ user: string[], roles: string[] } | undefined} performers given for element */ -export function getPerformersFromElement(element: object): any[]; +export function getPerformersFromElement(element: object): + | { + user: string[]; + roles: string[]; + } + | undefined; /** * Get the performers for given element id * * @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object * @param {string} elementId the id of the element - * @returns {Array} array with all performers + * @returns {{ user: string[], roles: string[] } | undefined} array with all performers */ -export function getPerformersFromElementById(bpmn: string | object, elementId: string): any[]; +export function getPerformersFromElementById( + bpmn: string | object, + elementId: string, +): + | { + user: string[]; + roles: string[]; + } + | undefined; +/** + * Get the responsible persons for given element + * + * @param {object} element + * @returns {{ user: string[], roles: string[] } | undefined} responsible persons given for element + */ +export function getResponsiblePartyFromElement(element: object): + | { + user: string[]; + roles: string[]; + } + | undefined; +/** + * Get the performers for given element id + * + * @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object + * @param {string} elementId the id of the element + * @returns {{ user: string[], roles: string[] } | undefined} array with all responsible persons + */ +export function getResponsiblePartyFromElementId( + bpmn: string | object, + elementId: string, +): + | { + user: string[]; + roles: string[]; + } + | undefined; /** * Returrns the roles and users that may be owners of a specific element * diff --git a/src/helper-modules/bpmn-helper/src/getters.js b/src/helper-modules/bpmn-helper/src/getters.js index 4e4f648ce..739917c78 100644 --- a/src/helper-modules/bpmn-helper/src/getters.js +++ b/src/helper-modules/bpmn-helper/src/getters.js @@ -856,7 +856,7 @@ async function getVariablesFromElementById(bpmn, elementId) { * * @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object * @param {string} elementId the id of the element - * @returns {Array} array with all performers + * @returns {{ user: string[], roles: string[] } | undefined} array with all performers */ async function getPerformersFromElementById(bpmn, elementId) { const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn; @@ -865,6 +865,20 @@ async function getPerformersFromElementById(bpmn, elementId) { return getPerformersFromElement(element); } +/** + * Get the performers for given element id + * + * @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object + * @param {string} elementId the id of the element + * @returns {{ user: string[], roles: string[] } | undefined} array with all responsible persons + */ +async function getResponsiblePartyFromElementId(bpmn, elementId) { + const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn; + const element = getElementById(bpmnObj, elementId); + + return getResponsiblePartyFromElement(element); +} + /** * An object containing properties from defined companies * @@ -1169,7 +1183,7 @@ function getResourcesFromElement(element) { * Get the performers for given element * * @param {object} element - * @returns {Array} performers given for element + * @returns {{ user: string[], roles: string[] } | undefined} performers given for element */ function getPerformersFromElement(element) { if (element.resources) { @@ -1186,7 +1200,29 @@ function getPerformersFromElement(element) { return JSON.parse(potentialOwner.resourceAssignmentExpression.expression.body); } } - return []; +} + +/** + * Get the responsible persons for given element + * + * @param {object} element + * @returns {{ user: string[], roles: string[] } | undefined} responsible persons given for element + */ +function getResponsiblePartyFromElement(element) { + if (element.resources) { + const responsiblePerson = element.resources.find( + (resource) => resource.$type === 'proceed:ResponsibleParty', + ); + + if ( + responsiblePerson && + responsiblePerson.resourceAssignmentExpression && + responsiblePerson.resourceAssignmentExpression.expression && + responsiblePerson.resourceAssignmentExpression.expression.body + ) { + return JSON.parse(responsiblePerson.resourceAssignmentExpression.expression.body); + } + } } /** @@ -1346,6 +1382,8 @@ module.exports = { getLocationsFromElement, getPerformersFromElement, getPerformersFromElementById, + getResponsiblePartyFromElement, + getResponsiblePartyFromElementId, getPotentialOwnersFromElementById, parseISODuration, convertISODurationToMiliseconds, 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..a94be4504 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.module.scss @@ -0,0 +1,28 @@ +.ElementText { + word-break: normal; +} + +.ElementKeyText { + color: gray; + font-size: 0.8em; +} + +.ElementValueText { + font-size: 0.9em; +} + +.NewElementSection { + width: 100%; + padding: 0; + margin: 0; + + .ElementSectionDivider { + padding: 0; + margin: 0; + } +} + +.ElementSectionHeader { + font-weight: 600; + 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..d68a7faf6 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-details.tsx @@ -0,0 +1,638 @@ +import { CSSProperties, ReactNode, useEffect, useMemo, useState } from 'react'; +import { App, 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, + getResponsiblePartyFromElement, + 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 { getSpaceUsers } from '@/lib/data/users'; +import { User } from '@prisma/client'; +import cn from 'classnames'; +import { useEnvironment } from '@/components/auth-can'; +import { truthyFilter } from '@/lib/typescript-utils'; +import { isUserErrorResponse } from '@/lib/user-error'; +import { useQuery } from '@tanstack/react-query'; +const TextViewer = dynamic(() => import('@/components/text-viewer'), { ssr: false }); + +type VersionInfo = { + versionId?: string | undefined; + name?: string | undefined; + description?: string | undefined; + versionBasedOn?: string | undefined; + versionCreatedOn?: string | undefined; +}; + +type EntryTextProps = React.ComponentProps; +const EntryKeyText = ({ className, ...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 [previousVersionName, setPreviousVersionName] = useState(''); + 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); + const environment = useEnvironment(); + const { message } = App.useApp(); + + const { data: spaceUsers } = useQuery({ + queryFn: async () => { + const users = await getSpaceUsers(environment.spaceId); + if (isUserErrorResponse(users)) { + message.error(`Failed to load the users in the space ${users.error.message}`); + return []; + } + + return users; + }, + queryKey: ['space', environment.spaceId, environment.isOrganization, 'users'], + }); + + 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); + + if (element) { + if (element.type === 'bpmn:Process') { + const responsibleIds = getResponsiblePartyFromElement( + getElementById(bpmnObj, element.id), + ); + const responsible = responsibleIds?.user + .map((uId) => spaceUsers?.find((e) => e.id === uId)) + .filter(truthyFilter); + setResponsibleParty(responsible || []); + } else { + const elementPerformers = getPerformersFromElement(getElementById(bpmnObj, element.id)); + const performers = elementPerformers?.user + .map((uId) => spaceUsers?.find((e) => e.id === uId)) + .filter(truthyFilter); + setPerformers(performers || []); + } + } + } + getBpmnObject(); + }, [version, element, spaceUsers]); + + useEffect(() => { + async function getVersionData() { + const bpmnObj = await toBpmnObject(version.bpmn); + const defInfos = await getDefinitionsInfos(bpmnObj); + const defVersionInfos = await getDefinitionsVersionInformation(bpmnObj); + + setDefinitionsInfos(defInfos); + setDefinitionsVersionInfos(defVersionInfos); + + const previous = defVersionInfos.versionBasedOn + ? await getProcessVersion(environment.spaceId, processId, defVersionInfos.versionBasedOn) + : undefined; + + if (isUserErrorResponse(previous)) message.error(previous.error.message); + else setPreviousVersionName(previous?.name || ''); + } + getVersionData(); + }, [version, processId, environment, message]); + + const actualOwner = useMemo(() => { + if (token) { + return token.actualOwner; + } else if (logInfo) { + return logInfo.actualOwner; + } + + return []; + }, [token, logInfo]); + + const performerInfos = useMemo(() => { + let infos; + if (logInfo) { + infos = logInfo.performers?.user || []; + } else if (performers) { + infos = performers; + } + + return infos + ?.filter((i) => !i.isGuest) + .map((i) => ({ id: i.id, name: (i.firstName + ' ' + i.lastName).trim() })); + }, [logInfo, performers]); + + // 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 Manager, + + + {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, + {previousVersionName}, + ]); + 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, + + {performerInfos ? ( + !!performerInfos.length && ( + + {performerInfos.map(({ id, name }) => ( + + {name} + + ))} + + ) + ) : ( + + )} + , + ]); + + detailsEntries.push([ + Done Bye, + + {actualOwner?.map((e) => ( + + {e.fullName} + + ))} + , + ]); + + if (techDetails) { + detailsEntries.push([ + Username, + + {actualOwner?.map((e) => e.username).toString()} + , + ]); + detailsEntries.push([ + User ID, + + {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..ba5ebf9d6 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/element-overview.tsx @@ -0,0 +1,371 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { + Col, + Divider, + Image, + message, + Progress, + ProgressProps, + Row, + Space, + Typography, +} from 'antd'; +import { getDefinitionsInfos, 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..c843ff7b1 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,49 @@ 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, 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 { 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 +60,55 @@ export default function InstanceInfoPanel({ open: boolean; processId: string; version: { bpmn: string }; - instance?: ExtendedInstanceInfo; + instance?: { + engines: { + id: string; + online: boolean; + }[]; + processInstanceInitiator?: any; + processInstanceInitiatorSpaceId?: 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..37d618501 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-selector.tsx @@ -0,0 +1,12 @@ +import { Menu, Typography } from 'antd'; + +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..95e9a4aa7 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 @@ -1,8 +1,20 @@ -import React, { useState } from 'react'; +import React, { useMemo, 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, + 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 +22,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 +31,11 @@ type InstanceVariableProps = { refetch: () => void; }; +type FieldTitleProps = React.ComponentProps; +const FormFieldTitle = (props: FieldTitleProps) => ( + +); + const InstanceVariables: React.FC = ({ processId, version, @@ -33,10 +51,16 @@ 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 variableToEditDefinitions = useMemo(() => { + if (!variableToEdit) return undefined; + + return variableDefinitions.find((d) => d.name === variableToEdit.name); + }, [variableToEdit, variableDefinitions]); + const columns: React.ComponentProps>['columns'] = [ { title: 'Name', dataIndex: 'name', key: 'name' }, { @@ -130,10 +154,18 @@ const InstanceVariables: React.FC = ({ /> + Change value of{' '} + + {variableToEdit?.name} + + + } onCancel={handleClose} destroyOnHidden okButtonProps={{ loading: submitting }} + okText={'Save value'} onOk={async () => { await form.validateFields(); @@ -172,65 +204,158 @@ const InstanceVariables: React.FC = ({ }); }} > -
- + + Current Value + + + - {updatedValueInput} - - + ]} + > + {updatedValueInput} +
+ + +
+ + +
+ Type +
+ Text +
+ + +
+ Format +
+ + {variableToEditDefinitions?.dataType === 'string' + ? variableToEditDefinitions?.textFormat + : variableToEditDefinitions?.dataType} + +
+ +
+ + +
+ Required at start +
+ + {variableToEditDefinitions?.requiredAtInstanceStartup ? 'Yes' : 'No'} + +
+ + +
+ Can be changed +
+ + {variableToEditDefinitions?.const ? 'No' : 'Yes'} + +
+ +
+ + +
+ Default value +
+ + {variableToEditDefinitions?.defaultValue} + +
+ + +
+ Allowed values +
+ {variableToEdit?.allowed} +
+ +
+ + + desc + + + + + + {variableToEditDefinitions?.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 537ab77b2..17e65db4a 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,374 +289,405 @@ 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={ + !!selectedInstanceId && !currentInstance + ? 'Fetching Instance Data' + : 'Select an instance' + } + /> + + {currentInstance?.offline && ( + + } - }, - selectedKeys: selectedVersionId ? [selectedVersionId] : [], - }} - > - - - - - - , - selectable: true, - onSelect: (item) => { - setSelectedColoring(item.key as ColorOptions); - }, - selectedKeys: [selectedColoring], - }} - > - + - )} - - {/* Activate/Deactivate button which is only shown when timer start events exist */} - {hasTimerStartEvents && ( - - + /> - + + )} + + + + + {enableInstanceCSVExport && ( + <> + + + + + + + + )} + {currentInstance && ( + + /> - - )} - {currentInstance && ( - + )} +