From 46ecdcf660710633aa22e87db2dd445ab0d9dda3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 3 Dec 2024 15:47:02 +0100 Subject: [PATCH 1/4] allow import of json to create processes for HTA2 project --- .../components/process-import.tsx | 247 +++++++++++++++++- 1 file changed, 243 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/components/process-import.tsx b/src/management-system-v2/components/process-import.tsx index e55fc28da..2baa0c32a 100644 --- a/src/management-system-v2/components/process-import.tsx +++ b/src/management-system-v2/components/process-import.tsx @@ -6,15 +6,24 @@ import { Button, Upload } from 'antd'; import type { ButtonProps } from 'antd'; import { - getDefinitionsId, + generateBpmnId, getDefinitionsName, getProcessDocumentation, + initXml, + setProceedElement, toBpmnObject, } from '@proceed/bpmn-helper'; import ProcessModal from './process-modal'; import { addProcesses } from '@/lib/data/processes'; import { useRouter } from 'next/navigation'; import { useEnvironment } from './auth-can'; +import BpmnModeler from 'bpmn-js/lib/Modeler'; +import type { ShapeLike } from 'diagram-js/lib/core/Types'; +import type ElementRegistry from 'diagram-js/lib/core/ElementRegistry'; +import type ElementFactory from 'diagram-js/lib/core/ElementFactory'; +import type Canvas from 'diagram-js/lib/core/Canvas'; +import type BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory'; +import type Modeling from 'bpmn-js/lib/features/modeling/Modeling'; export type ProcessData = { name: string; @@ -22,6 +31,27 @@ export type ProcessData = { bpmn: string; }; +type JSONProcessSchema = { + work_plan: { + product_name: string; + product_id: string; + process: JSONProcessTaskSchema[]; + }; +}; + +type JSONProcessTaskSchema = { + process_id: number; + process_name: string; + process_position_ID: number; + production_facility: Array; + production_process: string; + results: Array<{ + feature_name: string; + feature_id: number; + face_ID: Array; + }>; +}; + // TODO: maybe show import errors and warnings like in the old MS (e.g. id collisions if an existing process is reimported or two imports use the same id) const ProcessImportButton: React.FC = ({ ...props }) => { @@ -29,17 +59,226 @@ const ProcessImportButton: React.FC = ({ ...props }) => { const router = useRouter(); const environment = useEnvironment(); + const createTaskShape = ( + bpmnModeler: BpmnModeler, + taskInfo: JSONProcessTaskSchema, + taskPosition: { x: number; y: number }, + ) => { + const modeling = bpmnModeler.get('modeling') as Modeling; + const elementFactory = bpmnModeler.get('elementFactory') as ElementFactory; + const bpmnFactory = bpmnModeler.get('bpmnFactory') as BpmnFactory; + const rootElement = (bpmnModeler.get('canvas') as Canvas).getRootElement(); + + let taskDescription = + '| feature_name | feature_id | face_ID |' + '\n' + '| ------------ | ---------- | ------- |'; + + taskInfo.results.forEach((result: any) => { + taskDescription += + '\n' + `| ${result.feature_name} | ${result.feature_id} | ${result.face_ID.join(', ')} |`; + }); + + const isUserTask = taskInfo.production_facility.includes('Human'); + const taskShape = modeling.createShape( + elementFactory.createShape({ + type: isUserTask ? 'bpmn:UserTask' : 'bpmn:ServiceTask', + }), + taskPosition, + rootElement as any, + ); + taskShape.businessObject.name = taskInfo.process_name + .replace(/_/g, ' ') + .replace(/^./, (char) => char.toUpperCase()); + taskShape.businessObject.documentation = [ + bpmnFactory.create('bpmn:Documentation', { + text: taskDescription, + }), + ]; + setProceedElement(taskShape.businessObject, 'property', taskInfo.production_facility[0], { + name: 'production_facility', + }); + setProceedElement(taskShape.businessObject, 'property', taskInfo.production_process, { + name: 'production_process', + }); + + return taskShape; + }; + + const importJsonProcess = async (json: string) => { + const processInfo = JSON.parse(json) as JSONProcessSchema; + + const sortedTasks = processInfo.work_plan.process + .sort((taskA, taskB) => taskA.process_position_ID - taskB.process_position_ID) + .reduce<{ positionID: number; tasks: JSONProcessTaskSchema[] }[]>((acc, curr) => { + const isAlreadyAdded = acc.find((t) => t.positionID === curr.process_position_ID); + if (isAlreadyAdded) { + return acc; + } + + const parallelTasks = processInfo.work_plan.process.filter( + (t) => t.process_position_ID === curr.process_position_ID, + ); + + return [...acc, { positionID: curr.process_position_ID, tasks: parallelTasks }]; + }, []); + + const processId = `Process_${generateBpmnId()}`; + const startEventId = `StartEvent_${generateBpmnId()}`; + const bpmn = initXml(processId, startEventId); + + const bpmnModeler = new BpmnModeler(); + await bpmnModeler.importXML(bpmn); + + const modeling = bpmnModeler.get('modeling') as Modeling; + const elementRegistry = bpmnModeler.get('elementRegistry') as ElementRegistry; + const elementFactory = bpmnModeler.get('elementFactory') as ElementFactory; + const rootElement = (bpmnModeler.get('canvas') as Canvas).getRootElement() as any; + + setProceedElement(rootElement.businessObject, 'property', processInfo.work_plan.product_name, { + name: 'product_name', + }); + + const startEvent = elementRegistry.get(startEventId)! as ShapeLike; + const yPosition = startEvent.y + startEvent.height / 2; + const xPositionOffset = 150; + const yPositionOffset = 150; + + sortedTasks.reduce<{ + createdShapes: ShapeLike[]; + currentXPosition: number; + }>( + ({ createdShapes, currentXPosition }, { tasks: currentPositionIDTasks }, sortedTaskIdx) => { + const newShapes: ShapeLike[] = []; + let newCurrentXPosition = currentXPosition; + + if (currentPositionIDTasks.length > 1) { + // Create Parallel Gateway for parallel Tasks with same position ID + const parallelGatewayOutgoing = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:ParallelGateway', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + + const minYPosition = + currentPositionIDTasks.length % 2 === 0 + ? yPosition + + yPositionOffset / 2 - + (currentPositionIDTasks.length / 2) * yPositionOffset + : yPosition - Math.floor(currentPositionIDTasks.length / 2) * yPositionOffset; + + const parallelTaskShapes: ShapeLike[] = []; + newCurrentXPosition += xPositionOffset; + + currentPositionIDTasks.forEach((task, index) => { + const parallelTaskShape = createTaskShape(bpmnModeler, task, { + x: newCurrentXPosition, + y: minYPosition + yPositionOffset * index, + }); + modeling.createConnection( + parallelGatewayOutgoing, + parallelTaskShape, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + parallelTaskShapes.push(parallelTaskShape); + }); + + const parallelGatewayIncoming = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:ParallelGateway', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + + parallelTaskShapes.forEach((taskShape) => { + modeling.createConnection( + taskShape as any, + parallelGatewayIncoming, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + }); + + newShapes.push(parallelGatewayOutgoing, ...parallelTaskShapes, parallelGatewayIncoming); + } else { + const task = currentPositionIDTasks[0]; + const taskShape = createTaskShape(bpmnModeler, task, { + x: (newCurrentXPosition += xPositionOffset), + y: yPosition, + }); + + newShapes.push(taskShape); + } + + // Connect shape of previous iteration to first shape created in this iteration (either task or outgoing parallel gateway) + modeling.createConnection( + createdShapes[createdShapes.length - 1] as any, + newShapes[0] as any, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + if (sortedTaskIdx === sortedTasks.length - 1) { + // Create end event and connect to last shape created in this iteration + const endEvent = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:EndEvent', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + modeling.createConnection( + newShapes[newShapes.length - 1] as any, + endEvent, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + newShapes.push(endEvent); + } + + return { + createdShapes: [...createdShapes, ...newShapes], + currentXPosition: newCurrentXPosition, + }; + }, + { createdShapes: [startEvent], currentXPosition: startEvent.x }, + ); + + const { xml } = await bpmnModeler.saveXML({ format: true }); + + if (!xml) { + throw new Error('Could not retrieve XML from modeler'); + } + + return xml; + }; + return ( <> { const processesData = await Promise.all( fileList.map(async (file) => { - // get the bpmn from the file and then extract relevant process meta data from the bpmn - const bpmn = await file.text(); + const fileText = await file.text(); + const bpmn = + process.env.NEXT_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' + ? await importJsonProcess(fileText) + : fileText; const bpmnObj = await toBpmnObject(bpmn); From 4f68a2070c99c5eb7ee46154752be80ad3ffa55d Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 5 Dec 2024 15:00:25 +0100 Subject: [PATCH 2/4] added env variable for HTA projects --- src/management-system-v2/lib/env-vars.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/management-system-v2/lib/env-vars.ts b/src/management-system-v2/lib/env-vars.ts index 70eb4f3ba..3a8447178 100644 --- a/src/management-system-v2/lib/env-vars.ts +++ b/src/management-system-v2/lib/env-vars.ts @@ -11,6 +11,7 @@ const environmentVariables = { NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), ENABLE_MACHINE_CONFIG: z.string().optional(), // NOTE: Not sure if it should be optional NEXT_PUBLIC_ENABLE_EXECUTION: z.string().optional(), + NEXT_PUBLIC_PROJECTS_HTA2: z.string().optional(), NEXTAUTH_URL: z.string().default('http://localhost:3000'), INVITATION_ENCRYPTION_SECRET: z.string(), MS_ENABLED_RESOURCES: z From f714fc79f71aa6eb2bc16189f21d6bf913ea6b58 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 15 Jan 2025 17:05:19 +0100 Subject: [PATCH 3/4] use new env variable --- src/management-system-v2/components/process-import.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/components/process-import.tsx b/src/management-system-v2/components/process-import.tsx index 2baa0c32a..b3191592e 100644 --- a/src/management-system-v2/components/process-import.tsx +++ b/src/management-system-v2/components/process-import.tsx @@ -268,7 +268,7 @@ const ProcessImportButton: React.FC = ({ ...props }) => { return ( <> { @@ -276,7 +276,7 @@ const ProcessImportButton: React.FC = ({ ...props }) => { fileList.map(async (file) => { const fileText = await file.text(); const bpmn = - process.env.NEXT_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' + process.env.PROCEED_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' ? await importJsonProcess(fileText) : fileText; From d17be67350f32309d9a227085583b1896adae2a3 Mon Sep 17 00:00:00 2001 From: Kai Rohwer Date: Tue, 13 May 2025 14:20:40 +0200 Subject: [PATCH 4/4] feat(process-import): enhance JSON process schema and integrate environment variables --- .../components/process-import.tsx | 128 ++++++++++++++++-- 1 file changed, 116 insertions(+), 12 deletions(-) diff --git a/src/management-system-v2/components/process-import.tsx b/src/management-system-v2/components/process-import.tsx index b3191592e..40d3b531e 100644 --- a/src/management-system-v2/components/process-import.tsx +++ b/src/management-system-v2/components/process-import.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, use } from 'react'; import { Button, Upload } from 'antd'; import type { ButtonProps } from 'antd'; @@ -24,6 +24,7 @@ import type ElementFactory from 'diagram-js/lib/core/ElementFactory'; import type Canvas from 'diagram-js/lib/core/Canvas'; import type BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory'; import type Modeling from 'bpmn-js/lib/features/modeling/Modeling'; +import { EnvVarsContext } from './env-vars-context'; export type ProcessData = { name: string; @@ -37,24 +38,122 @@ type JSONProcessSchema = { product_id: string; process: JSONProcessTaskSchema[]; }; + features: { + [key: string]: { + general: { + bore_length: number; + bore_radius: number; + id: number; + type: string; + }; + geometry: { + [key: string]: { + geometry: { + angle: number; + area: number; + 'face type': string; + length: number; + radius: number; + stepEntity: string; + stepID: number; + }; + tolerances: { + actualHorizontal: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + actualVertical: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + targetHorizontal: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + targetVertical: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + }; + }; + }; + }; + }; + production_facilities: { + production_facilities: Array<{ + production_facility: string; + process: Array<{ + process_position_id: number; + production_process: string; + description: string; + parameter: { + when: string; + manual_automated: string; + 'machine hour rate [€/h]': string; + }; + result: string; + }>; + }>; + }; }; type JSONProcessTaskSchema = { process_id: number; process_name: string; - process_position_ID: number; - production_facility: Array; + process_position_id: number; production_process: string; + production_facility: { + [key: string]: { + facility_reference: string; + 'total processing time [min]': string; + 'total processing costs [€]': string; + parameter: Array<{ + facility_reference: 'production_facilities.Chisel_system'; + 'total processing time [min]': ''; + 'total processing costs [€]': ''; + parameter: Array<{ + Schritt: number; + 'Zeit von [min]': number; + 'Zeit bis [min]': number; + Werkzeug: string; + 'v_w [m/s]': number; + 'z_w [mm]': number; + 'f_a [Hz]': number; + 'Ra von': number; + 'Ra bis': number; + }>; + }>; + }; + }; results: Array<{ feature_name: string; - feature_id: number; - face_ID: Array; + face_ID: { + [key: string]: { geometry: string }; + }; + general: string; }>; }; // TODO: maybe show import errors and warnings like in the old MS (e.g. id collisions if an existing process is reimported or two imports use the same id) const ProcessImportButton: React.FC = ({ ...props }) => { + const env = use(EnvVarsContext); const [importProcessData, setImportProcessData] = useState([]); const router = useRouter(); const environment = useEnvironment(); @@ -62,6 +161,7 @@ const ProcessImportButton: React.FC = ({ ...props }) => { const createTaskShape = ( bpmnModeler: BpmnModeler, taskInfo: JSONProcessTaskSchema, + processInfo: JSONProcessSchema, taskPosition: { x: number; y: number }, ) => { const modeling = bpmnModeler.get('modeling') as Modeling; @@ -77,7 +177,10 @@ const ProcessImportButton: React.FC = ({ ...props }) => { '\n' + `| ${result.feature_name} | ${result.feature_id} | ${result.face_ID.join(', ')} |`; }); - const isUserTask = taskInfo.production_facility.includes('Human'); + const facility = processInfo.production_facilities.production_facilities.find( + (entry) => entry.production_facility === Object.keys(taskInfo.production_facility)[0], + ); + const isUserTask = facility?.process; const taskShape = modeling.createShape( elementFactory.createShape({ type: isUserTask ? 'bpmn:UserTask' : 'bpmn:ServiceTask', @@ -104,21 +207,22 @@ const ProcessImportButton: React.FC = ({ ...props }) => { }; const importJsonProcess = async (json: string) => { + console.log('importJsonProcess', json); const processInfo = JSON.parse(json) as JSONProcessSchema; const sortedTasks = processInfo.work_plan.process - .sort((taskA, taskB) => taskA.process_position_ID - taskB.process_position_ID) + .sort((taskA, taskB) => taskA.process_position_id - taskB.process_position_id) .reduce<{ positionID: number; tasks: JSONProcessTaskSchema[] }[]>((acc, curr) => { - const isAlreadyAdded = acc.find((t) => t.positionID === curr.process_position_ID); + const isAlreadyAdded = acc.find((t) => t.positionID === curr.process_position_id); if (isAlreadyAdded) { return acc; } const parallelTasks = processInfo.work_plan.process.filter( - (t) => t.process_position_ID === curr.process_position_ID, + (t) => t.process_position_ID === curr.process_position_id, ); - return [...acc, { positionID: curr.process_position_ID, tasks: parallelTasks }]; + return [...acc, { positionID: curr.process_position_id, tasks: parallelTasks }]; }, []); const processId = `Process_${generateBpmnId()}`; @@ -268,7 +372,7 @@ const ProcessImportButton: React.FC = ({ ...props }) => { return ( <> { @@ -276,7 +380,7 @@ const ProcessImportButton: React.FC = ({ ...props }) => { fileList.map(async (file) => { const fileText = await file.text(); const bpmn = - process.env.PROCEED_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' + env.PROCEED_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' ? await importJsonProcess(fileText) : fileText;