diff --git a/src/engine/universal/core/src/engine/engine.js b/src/engine/universal/core/src/engine/engine.js index 027ceaddb..c3a3a73fe 100644 --- a/src/engine/universal/core/src/engine/engine.js +++ b/src/engine/universal/core/src/engine/engine.js @@ -242,6 +242,16 @@ class Engine { } } + /** + * Returns whether the given process version is currently deployed in the NeoBPMN Engine + * + * @param {string} versionId the version of the process to check + * @returns {boolean} true if the version is deployed, false if it is known but not active + */ + async isProcessVersionDeployed(versionId) { + return this._versionProcessMapping[versionId]?.isDeployed() ?? false; + } + /** * Starts the execution of a BPMN process version. This can involve the creation of * multiple instances of the process, if the process contains such events. diff --git a/src/engine/universal/distribution/src/routes/ProcessInstanceRoutes.js b/src/engine/universal/distribution/src/routes/ProcessInstanceRoutes.js index 042b66099..d8cabf689 100644 --- a/src/engine/universal/distribution/src/routes/ProcessInstanceRoutes.js +++ b/src/engine/universal/distribution/src/routes/ProcessInstanceRoutes.js @@ -460,4 +460,28 @@ module.exports = (path, management) => { response: '{}', }; }); + + network.get(`${path}/:definitionId/versions/:version/active`, { cors: true }, async (req) => { + const { definitionId, version } = req.params; + + try { + await db.getProcessVersion(definitionId, version); + } catch { + return { + statusCode: 404, + mimeType: 'text/plain', + response: 'The requested version is not found in this engine.', + }; + } + + const engine = management.getEngineWithDefinitionId(definitionId); + + return { + statusCode: 200, + mimeType: 'application/json', + response: JSON.stringify({ + active: engine ? await engine.isProcessVersionDeployed(version) : false, + }), + }; + }); }; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.module.scss b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.module.scss index 71de67a40..54bd0f4c0 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.module.scss +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.module.scss @@ -9,3 +9,18 @@ .StopIcon { color: #ff4d4f; } + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.SpinIcon { + animation: spin 2s linear infinite; + display: flex; + align-items: center; +} 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 18ed6f770..21e2d16fa 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 @@ -1,7 +1,7 @@ // TODO: remove the use client if this page is used in server 'use client'; -import { Button, Select, Tooltip, Space, Dropdown, Result } from 'antd'; +import { Button, Select, Tooltip, Space, Dropdown, Result, App } from 'antd'; import Content from '@/components/content'; import BPMNCanvas, { BPMNCanvasRef } from '@/components/bpmn-canvas'; import { Toolbar, ToolbarGroup } from '@/components/toolbar'; @@ -13,13 +13,13 @@ import { PauseOutlined, StopOutlined, } from '@ant-design/icons'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import contentStyles from './content.module.scss'; import toolbarStyles from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.module.scss'; import styles from './process-deployment-view.module.scss'; import InstanceInfoPanel from './instance-info-panel'; import { useSearchParamState } from '@/lib/use-search-param-state'; -import { MdOutlineColorLens } from 'react-icons/md'; +import { MdOutlineColorLens, MdOutlineSync, MdOutlineSyncDisabled } from 'react-icons/md'; import { ColorOptions, colorOptions } from './instance-coloring'; import { RemoveReadOnly } from '@/lib/typescript-utils'; import type { ElementLike } from 'diagram-js/lib/core/Types'; @@ -33,10 +33,14 @@ import { DeployedProcessInfo } from '@/lib/engines/deployment'; import StartFormModal from './start-form-modal'; import useInstanceVariables from './use-instance-variables'; import { inlineScript, inlineUserTaskData } from '@proceed/user-task-helper'; -import { getGlobalVariablesForHTML } from '@/lib/engines/server-actions'; -import { useEnvironment } from '@/components/auth-can'; +import { toBpmnObject, getElementsByTagName } from '@proceed/bpmn-helper'; +import { + changeDeploymentActivation, + getProcessActivationStatus, + getGlobalVariablesForHTML, +} from '@/lib/engines/server-actions'; import { useSession } from 'next-auth/react'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { useEnvironment } from '@/components/auth-can'; export default function ProcessDeploymentView({ processId, @@ -45,6 +49,10 @@ export default function ProcessDeploymentView({ processId: string; initialDeploymentInfo: DeployedProcessInfo; }) { + const app = App.useApp(); + const { data: session } = useSession(); + const { spaceId } = useEnvironment(); + const [selectedVersionId, setSelectedVersionId] = useState(); const [selectedInstanceId, setSelectedInstanceId] = useSearchParamState('instance'); const [selectedColoring, setSelectedColoring] = useState('processColors'); @@ -54,15 +62,17 @@ export default function ProcessDeploymentView({ const [resumingInstance, setResumingInstance] = useState(false); const [pausingInstance, setPausingInstance] = useState(false); const [stoppingInstance, setStoppingInstance] = useState(false); + const [togglingActivation, setTogglingActivation] = useState(false); + const [isProcessActivated, setIsProcessActivated] = useState(false); + const [isActivationLoading, setIsActivationLoading] = useState(false); + const [hasTimerStartEvents, setHasTimerStartEvents] = useState(false); + const [hasPlainStartEvents, setHasPlainStartEvents] = useState(false); const [startForm, setStartForm] = useState(''); const canvasRef = useRef(null); const [infoPanelOpen, setInfoPanelOpen] = useState(false); - const { spaceId } = useEnvironment(); - const { data: session } = useSession(); - const { data: deploymentInfo, refetch, @@ -92,7 +102,12 @@ export default function ProcessDeploymentView({ if (deploymentInfo) { selectedVersion = deploymentInfo.versions.find((v) => v.versionId === selectedVersionId); - instances = getVersionInstances(deploymentInfo, selectedVersionId); + // sort instances newest first + const rawInstances = getVersionInstances(deploymentInfo, selectedVersionId); + instances = [...rawInstances].sort( + (a, b) => new Date(b.globalStartTime).getTime() - new Date(a.globalStartTime).getTime(), + ); + selectedInstance = selectedInstanceId ? instances.find((i) => i.processInstanceId === selectedInstanceId) : undefined; @@ -124,6 +139,67 @@ export default function ProcessDeploymentView({ }; }, [deploymentInfo, selectedVersionId, selectedInstanceId]); + useEffect(() => { + if (!currentVersion?.bpmn) return; + + async function initStartEventInfo() { + try { + const bpmnObj = await toBpmnObject(currentVersion!.bpmn); + const startEvents = await getElementsByTagName(bpmnObj, 'bpmn:StartEvent'); + let hasTimer = false; + let hasPlain = false; + (startEvents as any[]).forEach((el) => { + const defs = el?.eventDefinitions; + if (!defs || defs.length === 0) { + hasPlain = true; + } else if (defs.some((def: any) => def.$type === 'bpmn:TimerEventDefinition')) { + hasTimer = true; + } + }); + setHasTimerStartEvents(hasTimer); + setHasPlainStartEvents(hasPlain); + } catch (_) { + setHasTimerStartEvents(false); + setHasPlainStartEvents(false); + } + } + + initStartEventInfo(); + + return () => { + setHasTimerStartEvents(false); + setHasPlainStartEvents(false); + }; + }, [currentVersion?.bpmn]); + + useEffect(() => { + if (!currentVersion) return; + let cancelled = false; + + async function fetchActivationStatus() { + setIsActivationLoading(true); + await wrapServerCall({ + fn: () => getProcessActivationStatus(processId, spaceId, currentVersion!.versionId), + onSuccess: (active) => { + if (!cancelled) setIsProcessActivated(active as boolean); + }, + onError: (error) => { + if (!cancelled) { + app.message.error(error.message); + setIsProcessActivated(false); + } + }, + }); + if (!cancelled) setIsActivationLoading(false); + } + + fetchActivationStatus(); + + return () => { + cancelled = true; + }; + }, [currentVersion?.versionId]); + const { variableDefinitions, variables } = useInstanceVariables({ process: deploymentInfo, version: currentVersion, @@ -170,6 +246,7 @@ export default function ProcessDeploymentView({ alignItems: 'start', }} > + {/* Left group: Select Instance + Filter + Color */}