diff --git a/.github/workflows/deploy_with_db.yml b/.github/workflows/deploy_with_db.yml index 0a83a26fd..3ddd37601 100644 --- a/.github/workflows/deploy_with_db.yml +++ b/.github/workflows/deploy_with_db.yml @@ -36,7 +36,7 @@ jobs: FILE="FeatureFlags.js" # Define exempted flags - EXEMPTED_FLAGS=("enableMessaging" "enableUseFileManager" "enableInterruptedInstanceRecovery") + EXEMPTED_FLAGS=("enableMessaging" "enableUseFileManager" "enableInterruptedInstanceRecovery" "enableInstanceCSVExport") # Find all flags set to `true` FLAGS_WITH_TRUE=$(grep -E '^\s*enable[a-zA-Z0-9_]+\s*:\s*true' "$FILE" | awk -F: '{print $1}' | tr -d ' ') diff --git a/FeatureFlags.js b/FeatureFlags.js index 30563132f..297da2562 100644 --- a/FeatureFlags.js +++ b/FeatureFlags.js @@ -46,7 +46,7 @@ module.exports = { enableChatbot: false, // CSV export buttons for process instances - enableInstanceCSVExport: false, + enableInstanceCSVExport: true, //feature to use GCP_bucket / fs depending on deployment env to store blobs // ----------------------------------------------------------------------------- diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/dashboard-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/dashboard-view.tsx index ffb2e9631..d653ba075 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/dashboard-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/dashboard-view.tsx @@ -1,19 +1,64 @@ 'use client'; import { useMemo, Fragment } from 'react'; -import useDeployments from './use-deployments'; import { Card, Col, Row, Skeleton, Statistic } from 'antd'; +import { useEnvironment } from '@/components/auth-can'; +import { getAvailableSpaceEngines } from '@/lib/data/engines'; +import { useQuery } from '@tanstack/react-query'; +import { getDeployedProcesses } from '@/lib/data/deployment'; +import { isUserErrorResponse } from '@/lib/user-error'; +import { asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { getInstance } from '@/lib/data/instance'; +import { truthyFilter } from '@/lib/typescript-utils'; const DashboardView: React.FC = () => { - const { engines, deployments } = useDeployments( - 'definitionId,instances(processInstanceId,instanceState)', - ); + const space = useEnvironment(); + + const { data: engines } = useQuery({ + queryFn: async () => { + const res = await getAvailableSpaceEngines(space.spaceId); + + if (isUserErrorResponse(res)) return []; + + return res; + }, + refetchInterval: 1000, + queryKey: ['space', space.spaceId, 'engines'], + }); + + const { data } = useQuery({ + queryFn: async () => { + let deployedProcesses = await getDeployedProcesses(space.spaceId); + + if (isUserErrorResponse(deployedProcesses)) + return { deployedProcesses: [] as string[], instances: [] }; + + const instanceIds = new Set(); + deployedProcesses.forEach((p) => + p.versions.forEach((v) => + v.deployments.forEach((d) => d.instances.forEach((i) => instanceIds.add(i.id))), + ), + ); + + const instances = ( + await asyncMap([...instanceIds], async (id) => { + const instance = await getInstance(space.spaceId, id); + if (isUserErrorResponse(instance)) return undefined; + return instance; + }) + ).filter(truthyFilter); + + return { deployedProcesses: deployedProcesses.map((p) => p.id), instances }; + }, + refetchInterval: 1000, + queryKey: ['space', space.spaceId, 'deployments'], + }); const stats = useMemo(() => { - if (!engines || !deployments) return; + if (!engines || !data) return; const stats = { numEngines: 0, - numDeployments: 0, + numDeployments: data.deployedProcesses.length, numInstances: 0, numRunningInstances: 0, numFailedInstances: 0, @@ -32,33 +77,27 @@ const DashboardView: React.FC = () => { stats.numEngines = engines.length; - const knownDeployments: Record = {}; const knownInstances: Record = {}; - for (const { definitionId, instances } of deployments) { - if (!knownDeployments[definitionId]) { - stats.numDeployments++; - knownDeployments[definitionId] = true; - } - - for (const { processInstanceId, instanceState } of instances) { - if (!knownDeployments[processInstanceId]) { - knownInstances[processInstanceId] = instanceState; - } else { - knownInstances[processInstanceId].push(...instanceState); - } + for (const { + state: { processInstanceId, instanceState }, + } of data.instances) { + if (!knownInstances[processInstanceId]) { + knownInstances[processInstanceId] = instanceState; + } else { + knownInstances[processInstanceId].push(...instanceState); } + } - for (const instanceState of Object.values(knownInstances)) { - stats.numInstances++; + for (const instanceState of Object.values(knownInstances)) { + stats.numInstances++; - if (instanceState.some((state) => activeStates.includes(state))) { - stats.numRunningInstances++; - } else if (instanceState.some((state) => failedStates.includes(state))) { - stats.numFailedInstances++; - } else { - stats.numCompletedInstances++; - } + if (instanceState.some((state) => activeStates.includes(state))) { + stats.numRunningInstances++; + } else if (instanceState.some((state) => failedStates.includes(state))) { + stats.numFailedInstances++; + } else { + stats.numCompletedInstances++; } } @@ -90,7 +129,7 @@ const DashboardView: React.FC = () => { }, ], }; - }, [engines, deployments]); + }, [engines, data]); if (!stats) return ; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/use-deployments.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/use-deployments.ts deleted file mode 100644 index 739e0648d..000000000 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/use-deployments.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEnvironment } from '@/components/auth-can'; -import { getDeployments } from '@/lib/engines/deployment'; -import useEngines from '@/lib/engines/use-engines'; -import { useQuery } from '@tanstack/react-query'; -import { useCallback } from 'react'; - -function useDeployments(entries?: string) { - const space = useEnvironment(); - - const { data: engines } = useEngines(space); - - const queryFn = useCallback(async () => { - if (engines) { - return await getDeployments(engines, entries); - } - - return null; - }, [engines, entries]); - - const query = useQuery({ - queryFn, - queryKey: ['processDeployments', space.spaceId], - refetchInterval: 1000, - }); - - return { engines, deployments: query.data, ...query }; -} - -export default useDeployments; 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 index c4fac0b71..8ae809936 100644 --- 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 @@ -3,17 +3,27 @@ import { Alert, Checkbox, Image, Progress, ProgressProps, Space, Typography } fr import { ClockCircleFilled } from '@ant-design/icons'; import { getPlanDelays, getTimeInfo, statusToType } from './instance-helpers'; import { getMetaDataFromElement } from '@proceed/bpmn-helper'; -import { DisplayTable, RelevantInstanceInfo } from './instance-info-panel'; +import { DisplayTable } from './instance-info-panel'; import endpointBuilder from '@/lib/engines/endpoints/endpoint-builder'; import { generateDateString, generateDurationString, generateNumberString } from '@/lib/utils'; - -export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { +import { InstanceInfo } from '@/lib/engines/deployment'; +import type { ElementLike } from 'diagram-js/lib/core/Types'; + +export function ElementStatus({ + processId, + element, + instance, +}: { + processId: string; + element: ElementLike; + instance?: InstanceInfo; +}) { const statusEntries: ReactNode[][] = []; - const isRootElement = info.element && info.element.type === 'bpmn:Process'; - const metaData = getMetaDataFromElement(info.element.businessObject); - const token = info.instance?.tokens.find((l) => l.currentFlowElementId == info.element.id); - const logInfo = info.instance?.log.find((logEntry) => logEntry.flowElementId === info.element.id); + 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) @@ -35,7 +45,7 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { alt="Image linked to the element" src={endpointBuilder('get', '/resources/process/:definitionId/images/:fileName', { pathParams: { - definitionId: info.process.definitionId, + definitionId: processId, fileName: metaData.overviewImage, }, })} @@ -45,14 +55,14 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { // Element status let status = undefined; - if (isRootElement && info.instance) { - status = info.instance.instanceState[0]; - } else if (info.element && info.instance) { - const elementInfo = info.instance.log.find((l) => l.flowElementId == info.element.id); + 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 = info.instance.tokens.find((l) => l.currentFlowElementId == info.element.id); + const tokenInfo = instance.tokens.find((l) => l.currentFlowElementId == element.id); status = tokenInfo ? tokenInfo.currentFlowNodeState : 'WAITING'; } } @@ -73,7 +83,7 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { , ]); } @@ -81,7 +91,7 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { // Progress // TODO: editable progress // see src/management-system/src/frontend/components/deployments/activityInfo/ProgressSetter.vue - if (info.instance && !isRootElement) { + if (instance && !isRootElement) { let progress: | { value: number; manual: boolean; milestoneCalculatedProgress?: number } | undefined = undefined; @@ -116,10 +126,10 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { // User task // TODO: editable priority - if (info.element.type === 'bpmn:UserTask') { + if (element.type === 'bpmn:UserTask') { let priority: number | undefined = undefined; - if (info.instance) { + if (instance) { if (token) priority = token.priority; else if (logInfo) priority = logInfo.priority; } else { @@ -142,7 +152,7 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { // Real Costs // TODO: Set real costs - if (info.instance && !isRootElement) { + if (instance && !isRootElement) { let costs: string | undefined = undefined; if (token) costs = token.costsRealSetByOwner; else if (logInfo) costs = logInfo.costsRealSetByOwner; @@ -151,12 +161,12 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { } // Documentation - statusEntries.push(['Documentation:', info.element.businessObject?.documentation?.[0]?.text]); + statusEntries.push(['Documentation:', element.businessObject?.documentation?.[0]?.text]); // Activity time calculation const { start, end, duration } = getTimeInfo({ - element: info.element, - instance: info.instance, + element: element, + instance: instance, logInfo, token, }); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-helpers.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-helpers.ts index 1ac4c836c..27d228405 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-helpers.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-helpers.ts @@ -1,16 +1,7 @@ -import { getEnv, getSpaceUsers, getUser } from '@/lib/data/db/machine-config'; -import { DeployedProcessInfo, InstanceInfo, VersionInfo } from '@/lib/engines/deployment'; -import { asyncMap } from '@/lib/helpers/javascriptHelpers'; -import { truthyFilter } from '@/lib/typescript-utils'; -import { - getDefinitionsInfos, - getDefinitionsName, - getElementById, - toBpmnObject, -} from '@proceed/bpmn-helper'; +import { StoredDeployment } from '@/lib/data/deployment'; +import { InstanceInfo } from '@/lib/engines/deployment'; import { convertISODurationToMiliseconds } from '@proceed/bpmn-helper/src/getters'; import type { ElementLike } from 'diagram-js/lib/core/Types'; -import jsonToCsvExport from 'json-to-csv-export'; export type ElementStatus = | 'PAUSED' @@ -142,23 +133,21 @@ export function getPlanDelays({ return { plan, delays }; } -export function getVersionInstances(process: DeployedProcessInfo, version?: string) { - const instances = process.instances; - +export function getVersionInstances(instances: InstanceInfo[], version?: string) { if (!version) return instances; return instances.filter((instance) => instance.processVersion === version); } -export function getLatestDeployment(process: DeployedProcessInfo) { - let latest = process.versions.length - 1; - for (let i = process.versions.length - 2; i >= 0; i--) { - // TODO: this is actually the last version that was deployed since there is no version creation - // information stored on the engine (do we keep this, store the creation time on the engine or - // parse the creation time from the bpmn?) - if (process.versions[i].deploymentDate > process.versions[latest].deploymentDate) latest = i; - } - - return process.versions[latest]; +export function getLatestDeployment(deployments: StoredDeployment[]) { + return deployments.reduce( + (latest, curr) => { + if (!latest || latest.deployTime.getTime() > curr.deployTime.getTime()) { + return curr; + } + return latest; + }, + undefined as undefined | StoredDeployment, + ); } export function getYoungestInstance(instances: T) { @@ -170,151 +159,3 @@ export function getYoungestInstance(instances: T) { } return instances[firstInstance]; } - -export async function exportInstanceData( - selectedInstances: (InstanceInfo | undefined)[], - versionInfo: VersionInfo[], - spaceId: string, -) { - const objectOrderTemplate = { - ProcessId: null, - ProcessName: null, - ProcessShortName: null, - ProcessVersionId: null, - ProcessVersionName: null, - ProcessVersionDescription: null, - ProcessVersionCreatedOn: null, - ProcessVersionBasedOn: null, - ProcessInstanceId: null, - ProcessInstanceInitiatorId: null, - ProcessInstanceInitiatorFullName: null, - ProcessInstanceInitiatorUsername: null, - ProcessInstanceInitiatorSpaceId: null, - ProcessInstanceInitiatorSpaceName: null, - InstanceStartTime: null, - ProcessStepId: null, - ProcessStepName: null, - ProcessStepType: null, - ProcessStepStatus: null, - ProcessStepStartTime: null, - ProcessStepEndTime: null, - PreviousProcessStepId: null, - ProcessStepTokenId: null, - ActualPerformerId: null, - ActualPerformerName: null, - ActualPerformerUsername: null, - ProcessEngineId: null, - ProcessEngineName: null, //tofind - Log: null, - }; - - const spaceUsers = await getSpaceUsers(spaceId); - const space = await getEnv(spaceId); - - // pasting metadata from VersionInfo - const instancesWithVersionData = ( - await asyncMap(selectedInstances, async (instance) => { - if (!instance) return undefined; - const correspondingVersion = versionInfo.find((e) => e.versionId == instance.processVersion); - if (!correspondingVersion) return undefined; - const bpmnObj = await toBpmnObject(correspondingVersion.bpmn); - const initiator = spaceUsers.find((user) => user.id == instance.processInitiator); - const definitionInfos = await getDefinitionsInfos(bpmnObj); - - return { - ...instance, - ProcessName: definitionInfos.name, - ProcessShortName: definitionInfos.userDefinedId, - ProcessVersionName: correspondingVersion.versionName, - ProcessVersionDescription: correspondingVersion.versionDescription, - ProcessVersionCreatedOn: correspondingVersion.deploymentDate, - ProcessVersionBasedOn: correspondingVersion.basedOnVersion, - ProcessInstanceInitiatorFullName: - initiator && !initiator.isGuest - ? `${initiator.firstName} ${initiator.lastName}` - : 'Guest', - ProcessInstanceInitiatorUsername: - initiator && !initiator.isGuest ? initiator.username : 'Guest', - ProcessInstanceInitiatorSpaceName: space.isOrganization ? space.name : 'no organization', - ProcessEngineId: instance.log[0].machine.id, - correspondingVersion, - }; - }) - ).filter(truthyFilter); - - // retrieve and flatten event data - const instanceEvents = ( - await asyncMap(instancesWithVersionData, async (instance) => { - const bpmnObj = await toBpmnObject(instance.correspondingVersion?.bpmn || ''); - - return instance - ? instance.log.map((eventEntry) => { - const eventElement = getElementById(bpmnObj, eventEntry.flowElementId) as { - $type?: string; - name?: string; - outgoing?: any; - incoming?: any; - }; - const ActualPerformerId = eventEntry.actualOwner?.[0]; - const user = spaceUsers.find((user) => user.id == ActualPerformerId); - return { - ...instance, - ...eventEntry, - ProcessStepName: eventElement?.name, - ProcessStepType: eventElement?.$type?.split(':')[1], - ActualPerformerId, - ActualPerformerName: user ? `${user.firstName} ${user.lastName}` : undefined, - ActualPerformerUsername: user ? user.username : undefined, - Log: JSON.stringify(eventEntry.variableChanges), - PreviousProcessStepId: eventElement.incoming?.map((flow: any) => flow.sourceRef.id), - }; - }) - : []; - }) - ).flat(); - - // renaming - const keyMap: Record = { - processId: 'ProcessId', - processVersion: 'ProcessVersionId', - processInstanceId: 'ProcessInstanceId', - processInitiator: 'ProcessInstanceInitiatorId', - spaceIdOfProcessInitiator: 'ProcessInstanceInitiatorSpaceId', - globalStartTime: 'InstanceStartTime', - flowElementId: 'ProcessStepId', - executionState: 'ProcessStepStatus', - startTime: 'ProcessStepStartTime', - endTime: 'ProcessStepEndTime', - tokenId: 'ProcessStepTokenId', - }; - - const renamedInstanceEvents: Record[] = instanceEvents.map((instance) => - Object.fromEntries(Object.entries(instance).map(([k, v]) => [keyMap[k] ?? k, v])), - ); - - // converting dates - const datedInstanceEvents = renamedInstanceEvents.map((instance) => ({ - ...instance, - InstanceStartTime: new Date(instance.InstanceStartTime).toISOString(), - ProcessStepEndTime: new Date(instance.ProcessStepEndTime).toISOString(), - ProcessStepStartTime: new Date(instance.ProcessStepStartTime).toISOString(), - ProcessVersionCreatedOn: new Date(instance.ProcessVersionCreatedOn).toISOString(), - })); - - // reordering - const structuredInstanceEvents = datedInstanceEvents.map((instance) => - Object.entries(instance).reduce( - (acc, [key, value]) => { - if (key in acc) { - acc[key] = value; - } - return acc; - }, - { ...objectOrderTemplate } as Record, - ), - ); - - return jsonToCsvExport({ - data: structuredInstanceEvents, - }); -} 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 5e5009c84..ac9a0b8ee 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,19 +1,12 @@ import ResizableElement, { ResizableElementRefType } from '@/components/ResizableElement'; import CollapsibleCard from '@/components/collapsible-card'; import { ReactNode, useRef } from 'react'; -import { DeployedProcessInfo, InstanceInfo, VersionInfo } from '@/lib/engines/deployment'; +import { InstanceInfo } from '@/lib/engines/deployment'; import { Drawer, Grid, Tabs } from 'antd'; import type { ElementLike } from 'diagram-js/lib/core/Types'; import { ElementStatus } from './element-status'; import InstanceVariables from './instance-variables'; -export type RelevantInstanceInfo = { - instance?: InstanceInfo; - process: DeployedProcessInfo; - element: ElementLike; - version: VersionInfo; -}; - export function DisplayTable({ data }: { data: ReactNode[][] }) { // TODO: make this responsive return ( @@ -39,29 +32,35 @@ export function DisplayTable({ data }: { data: ReactNode[][] }) { export default function InstanceInfoPanel({ open, close, - info, + processId, + version, + instance, + element, refetch, }: { close: () => void; open: boolean; - info: RelevantInstanceInfo; + processId: string; + version: { bpmn: string }; + instance?: InstanceInfo; + element?: ElementLike; refetch: () => void; }) { const resizableElementRef = useRef(null); const breakpoints = Grid.useBreakpoint(); - const title = info.element?.businessObject?.name || info.element?.id || 'How to PROCEED?'; + const title = element?.businessObject?.name || element?.id || 'How to PROCEED?'; if (breakpoints.xl && !open) return null; - const tabs = info.element ? ( + const tabs = element ? ( , + children: , }, { key: 'Advanced', @@ -81,7 +80,14 @@ export default function InstanceInfoPanel({ { key: 'Variables', label: 'Variables', - children: , + children: ( + + ), }, { key: 'Resources', 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 d006f56be..0cce88fe9 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,5 +1,4 @@ import React, { useState } from 'react'; -import { RelevantInstanceInfo } from './instance-info-panel'; import { EditOutlined } from '@ant-design/icons'; @@ -10,13 +9,21 @@ import TextArea from 'antd/es/input/TextArea'; import { wrapServerCall } from '@/lib/wrap-server-call'; import useInstanceVariables, { Variable } from './use-instance-variables'; import { textFormatMap, typeLabelMap } from '@/lib/process-variable-schema'; +import { InstanceInfo } from '@/lib/engines/deployment'; type InstanceVariableProps = { - info: RelevantInstanceInfo; + processId: string; + version: { bpmn: string }; + instance: InstanceInfo | undefined; refetch: () => void; }; -const InstanceVariables: React.FC = ({ info, refetch }) => { +const InstanceVariables: React.FC = ({ + processId, + version, + instance, + refetch, +}) => { const [updatedValue, setUpdatedValue] = useState(undefined); const [submitting, setSubmitting] = useState(false); @@ -26,7 +33,7 @@ const InstanceVariables: React.FC = ({ info, refetch }) = const [form] = Form.useForm(); - const { variables } = useInstanceVariables(info); + const { variables } = useInstanceVariables({ version, instance }); const [variableToEdit, setVariableToEdit] = useState(undefined); @@ -56,7 +63,7 @@ const InstanceVariables: React.FC = ({ info, refetch }) = }, ]; - if (info.instance) { + if (instance) { columns.push({ title: '', key: 'edit', @@ -144,14 +151,9 @@ const InstanceVariables: React.FC = ({ info, refetch }) = wrapServerCall({ fn: async () => { setSubmitting(true); - return await updateVariables( - spaceId, - info.process.definitionId, - info.instance!.processInstanceId, - { - [variableToEdit!.name]: value, - }, - ); + return await updateVariables(spaceId, processId, instance!.processInstanceId, { + [variableToEdit!.name]: value, + }); }, onSuccess: () => { message.success('Applied Changes'); @@ -159,8 +161,12 @@ const InstanceVariables: React.FC = ({ info, refetch }) = setSubmitting(false); handleClose(); }, - onError: () => { - message.error('Could not apply changes'); + onError: (err) => { + if ('type' in err) { + message.error(err.message); + } else { + message.error('Could not apply changes'); + } setSubmitting(false); }, }); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx index d25de1d2b..ec3ac9f0f 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx @@ -1,23 +1,23 @@ import { Result, Skeleton } from 'antd'; import Content from '@/components/content'; -import { getDeployment } from '@/lib/executions/deployment-server-actions'; import ProcessDeploymentView from './process-deployment-view'; import { Suspense } from 'react'; import { getCurrentEnvironment } from '@/components/auth'; import { isUserErrorResponse } from '@/lib/user-error'; +import { getProcessDeployments } from '@/lib/data/deployment'; async function Deployment({ processId, spaceId }: { processId: string; spaceId: string }) { - const deployment = await getDeployment(spaceId, processId); + const deployments = await getProcessDeployments(spaceId, processId); - if (isUserErrorResponse(deployment)) { + if (isUserErrorResponse(deployments)) { return ( - + ); } - if (!deployment) { + if (!deployments.length) { return ( @@ -25,7 +25,7 @@ async function Deployment({ processId, spaceId }: { processId: string; spaceId: ); } - return ; + return ; } export default async function Page(props: { 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 929ee9a80..4b12d07a4 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, App } from 'antd'; +import { Button, Select, Tooltip, Space, Dropdown, Result, Skeleton } from 'antd'; import Content from '@/components/content'; import BPMNCanvas, { BPMNCanvasRef } from '@/components/bpmn-canvas'; import { Toolbar, ToolbarGroup } from '@/components/toolbar'; @@ -22,20 +22,13 @@ import InstanceInfoPanel from './instance-info-panel'; import { useSearchParamState } from '@/lib/use-search-param-state'; import { MdOutlineColorLens, MdOutlineSync, MdOutlineSyncDisabled } from 'react-icons/md'; import { ColorOptions, colorOptions } from './instance-coloring'; -import { RemoveReadOnly } from '@/lib/typescript-utils'; +import { RemoveReadOnly, truthyFilter } from '@/lib/typescript-utils'; import type { ElementLike } from 'diagram-js/lib/core/Types'; import { wrapServerCall } from '@/lib/wrap-server-call'; -import useDeployment from '../deployment-hook'; -import { - exportInstanceData, - getLatestDeployment, - getVersionInstances, - getYoungestInstance, -} from './instance-helpers'; +import { getLatestDeployment, getVersionInstances, getYoungestInstance } from './instance-helpers'; import useColors from './use-colors'; import useTokens from './use-tokens'; -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'; @@ -51,22 +44,23 @@ import { useEnvironment } from '@/components/auth-can'; import { GrDocumentUser } from 'react-icons/gr'; import { handleOpenDocumentation } from '../../../processes/processes-helper'; import { + exportInstanceData, getProcessStartForm, pauseInstance, resumeInstance, startInstance, stopInstance, } from '@/lib/executions/instance-server-actions'; +import { useQuery } from '@tanstack/react-query'; +import { getProcessDeployments } from '@/lib/data/deployment'; +import { isSuccessResponse, isUserErrorResponse, userError } from '@/lib/user-error'; +import { getInstance } from '@/lib/data/instance'; +import { asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { getProcessBPMN } from '@/lib/data/processes'; import { enableInstanceCSVExport } from 'FeatureFlags'; +import jsonToCsvExport from 'json-to-csv-export'; -export default function ProcessDeploymentView({ - processId, - initialDeploymentInfo, -}: { - processId: string; - initialDeploymentInfo: DeployedProcessInfo; -}) { - const app = App.useApp(); +export default function ProcessDeploymentView({ processId }: { processId: string }) { const { data: session } = useSession(); const { spaceId } = useEnvironment(); @@ -80,8 +74,6 @@ export default function ProcessDeploymentView({ 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); @@ -90,71 +82,149 @@ export default function ProcessDeploymentView({ const canvasRef = useRef(null); const [infoPanelOpen, setInfoPanelOpen] = useState(false); - const { data: deploymentInfo, refetch } = useDeployment(processId, initialDeploymentInfo); + // get information where the process is deployed and which instances exist + const { data: deployments, refetch: refetchDeployments } = useQuery({ + queryFn: async () => { + const deployments = await getProcessDeployments(spaceId, processId); + if (isUserErrorResponse(deployments)) return null; + return deployments; + }, + queryKey: ['processDeployments', spaceId, processId], + refetchInterval: 1000, + }); - const { - selectedVersion, - instances, - selectedInstance, - currentVersion, - instanceIsRunning, - instanceIsPausing, - instanceIsPaused, - } = useMemo(() => { - let selectedVersion, instances, selectedInstance, currentVersion; - let instanceIsRunning = false; - let instanceIsPausing = false; - let instanceIsPaused = false; + // keep a list of known instances to trigger refetches only when necessary + const instanceIds = useMemo(() => { + if (!deployments) return ''; - const activeStates = ['PAUSED', 'RUNNING', 'READY', 'DEPLOYMENT-WAITING', 'WAITING']; + const instanceMap = {} as Record; - if (deploymentInfo) { - selectedVersion = deploymentInfo.versions.find((v) => v.versionId === selectedVersionId); + for (const deployment of deployments) { + for (const instanceId of deployment.instances) { + instanceMap[instanceId] = true; + } + } + + return Object.keys(instanceMap).join(','); + }, [deployments]); + + // fetch initial data for all instances of the process when the list of instances changed + const { data: knownInstances, refetch: refetchInstances } = useQuery({ + queryFn: async () => { + if (!instanceIds) return []; + + const instances = ( + await asyncMap(instanceIds.split(','), async (instanceId) => + getInstance(spaceId, instanceId), + ) + ) + .filter(isSuccessResponse) + .filter(truthyFilter) + .map((i) => i.state); + + return instances; + }, + queryKey: ['processDeployments', spaceId, processId, 'instances', instanceIds], + initialData: [], + enabled: !!instanceIds, + }); + + const { selectedVersion, versionInstances, currentVersion } = useMemo(() => { + let selectedVersion, versionInstances, currentVersion; + + if (deployments?.length) { + selectedVersion = deployments.find((d) => d.versionId === selectedVersionId)?.version; // sort instances newest first - const rawInstances = getVersionInstances(deploymentInfo, selectedVersionId); - instances = [...rawInstances].sort( + const rawInstances = getVersionInstances(knownInstances, selectedVersionId); + versionInstances = [...rawInstances].sort( (a, b) => new Date(b.globalStartTime).getTime() - new Date(a.globalStartTime).getTime(), ); - selectedInstance = selectedInstanceId - ? instances.find((i) => i.processInstanceId === selectedInstanceId) + const selectedInstance = selectedInstanceId + ? versionInstances.find((i) => i.processInstanceId === selectedInstanceId) : undefined; - let currentVersionId = getLatestDeployment(deploymentInfo).versionId; + let currentVersionId = getLatestDeployment(deployments)!.versionId; if (selectedInstance) { currentVersionId = selectedInstance.processVersion; - instanceIsRunning = selectedInstance.instanceState.some((state) => - activeStates.includes(state), - ); - instanceIsPausing = selectedInstance.instanceState.some((state) => state === 'PAUSING'); - instanceIsPaused = selectedInstance.instanceState.some((state) => state === 'PAUSED'); } else if (selectedVersionId) { currentVersionId = selectedVersionId; } - currentVersion = deploymentInfo.versions.find( - (version) => version.versionId === currentVersionId, - ); + currentVersion = deployments.find((d) => d.versionId === currentVersionId)!.version; } return { selectedVersion, - instances, - selectedInstance, + versionInstances, currentVersion, - instanceIsRunning, - instanceIsPausing, - instanceIsPaused, }; - }, [deploymentInfo, selectedVersionId, selectedInstanceId]); + }, [deployments, knownInstances, selectedVersionId, selectedInstanceId]); - useEffect(() => { - if (!currentVersion?.bpmn) return; + const { data: currentInstance, refetch: refetchCurrentInstance } = useQuery({ + queryKey: ['processDeployments', spaceId, processId, 'instance', selectedInstanceId], + queryFn: async () => { + if (!selectedInstanceId) return null; + const instance = await getInstance(spaceId, selectedInstanceId); + if (isUserErrorResponse(instance)) return null; + if (!instance) return null; + + return { ...instance.state, engineIds: instance.engineIds }; + }, + enabled: !!selectedInstanceId, + refetchInterval: 1000, + }); + + const { instanceIsRunning, instanceIsPausing, instanceIsPaused } = useMemo(() => { + let instanceIsRunning = false; + let instanceIsPausing = false; + let instanceIsPaused = false; + + const activeStates = ['PAUSED', 'RUNNING', 'READY', 'DEPLOYMENT-WAITING', 'WAITING']; + + if (currentInstance) { + instanceIsRunning = currentInstance.instanceState.some((state) => + activeStates.includes(state), + ); + instanceIsPausing = currentInstance.instanceState.some((state) => state === 'PAUSING'); + instanceIsPaused = currentInstance.instanceState.some((state) => state === 'PAUSED'); + } + + return { instanceIsRunning, instanceIsPausing, instanceIsPaused }; + }, [currentInstance]); + + const { + data: isProcessActivated, + isFetching: isActivationLoading, + refetch: refetchActivation, + } = useQuery({ + queryFn: async () => { + if (!currentVersion) return false; + const status = await getProcessActivationStatus(processId, spaceId, currentVersion.id); + + if (isUserErrorResponse(status)) return false; + return status; + }, + queryKey: ['processActivation', spaceId, processId, currentVersion], + }); + + const { data: selectedBpmn } = useQuery({ + queryFn: async () => { + const bpmn = await getProcessBPMN(processId, spaceId, currentVersion?.id); + if (isUserErrorResponse(bpmn)) return undefined; + return { bpmn }; + }, + queryKey: ['space', spaceId, 'process', processId, 'version', currentVersion?.id || '', 'bpmn'], + }); + + useEffect(() => { async function initStartEventInfo() { + if (!selectedBpmn) return; + try { - const bpmnObj = await toBpmnObject(currentVersion!.bpmn); - const startEvents = await getElementsByTagName(bpmnObj, 'bpmn:StartEvent'); + const bpmnObj = await toBpmnObject(selectedBpmn.bpmn); + const startEvents = getElementsByTagName(bpmnObj, 'bpmn:StartEvent'); let hasTimer = false; let hasPlain = false; (startEvents as any[]).forEach((el) => { @@ -167,10 +237,7 @@ export default function ProcessDeploymentView({ }); setHasTimerStartEvents(hasTimer); setHasPlainStartEvents(hasPlain); - } catch (_) { - setHasTimerStartEvents(false); - setHasPlainStartEvents(false); - } + } catch (_) {} } initStartEventInfo(); @@ -179,50 +246,17 @@ export default function ProcessDeploymentView({ 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]); + }, [selectedBpmn]); const { variableDefinitions, variables } = useInstanceVariables({ - process: deploymentInfo, - version: currentVersion, + version: selectedBpmn, }); - const selectedBpmn = useMemo(() => { - return { bpmn: currentVersion?.bpmn || '' }; - }, [currentVersion]); - - const { refreshTokens } = useTokens(selectedInstance || null, canvasRef); + const { refreshTokens } = useTokens(currentInstance || null, canvasRef); const { refreshColoring } = useColors( selectedBpmn, selectedColoring, - selectedInstance, + currentInstance || undefined, canvasRef, ); const refreshVisuals = useCallback(() => { @@ -230,10 +264,18 @@ export default function ProcessDeploymentView({ refreshColoring(); }, [refreshTokens, refreshColoring]); - if (!deploymentInfo) { + if (!deployments || !selectedBpmn) { return ( - + + + ); + } + + if (!deployments.length) { + return ( + + ); } @@ -258,14 +300,10 @@ export default function ProcessDeploymentView({ {/* Left group: Select Instance + Filter + Color */}