diff --git a/FeatureFlags.js b/FeatureFlags.js index 56e3e08bc..30563132f 100644 --- a/FeatureFlags.js +++ b/FeatureFlags.js @@ -45,6 +45,9 @@ module.exports = { // Whether the Chatbot UserInterface and its functionality should be enabled enableChatbot: false, + // CSV export buttons for process instances + enableInstanceCSVExport: false, + //feature to use GCP_bucket / fs depending on deployment env to store blobs // ----------------------------------------------------------------------------- // Chopping Block 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 699c6c2e3..1ac4c836c 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,6 +1,16 @@ -import { DeployedProcessInfo, InstanceInfo } from '@/lib/engines/deployment'; +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 { 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' @@ -160,3 +170,151 @@ 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]/process-deployment-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx index 185059401..929ee9a80 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 @@ -12,6 +12,7 @@ import { CaretRightOutlined, PauseOutlined, StopOutlined, + ExportOutlined, } from '@ant-design/icons'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import contentStyles from './content.module.scss'; @@ -25,7 +26,12 @@ import { RemoveReadOnly } 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 { getLatestDeployment, getVersionInstances, getYoungestInstance } from './instance-helpers'; +import { + exportInstanceData, + getLatestDeployment, + getVersionInstances, + getYoungestInstance, +} from './instance-helpers'; import useColors from './use-colors'; import useTokens from './use-tokens'; @@ -51,6 +57,7 @@ import { startInstance, stopInstance, } from '@/lib/executions/instance-server-actions'; +import { enableInstanceCSVExport } from 'FeatureFlags'; export default function ProcessDeploymentView({ processId, @@ -497,6 +504,46 @@ export default function ProcessDeploymentView({ + {enableInstanceCSVExport && ( + <> + + + + + + + + )} {selectedInstance && (