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 afa2fe193..e9d858e16 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,7 @@ import { StoredDeployment } from '@/lib/data/deployment'; import { ExtendedInstanceInfo } from '@/lib/data/instance'; import { InstanceInfo } from '@/lib/engines/deployment'; +import { Version } from '@prisma/client'; import { convertISODurationToMiliseconds } from '@proceed/bpmn-helper/src/getters'; import type { ElementLike } from 'diagram-js/lib/core/Types'; @@ -179,15 +180,15 @@ export function getVersionInstances(instances: ExtendedInstanceInfo[], version?: return instances.filter((instance) => instance.processVersion === version); } -export function getLatestDeployment(deployments: StoredDeployment[]) { - return deployments.reduce( +export function getLatestVersion(versions: Version[]) { + return versions.reduce( (latest, curr) => { - if (!latest || latest.deployTime.getTime() > curr.deployTime.getTime()) { + if (!latest || latest.createdOn.getTime() < curr.createdOn.getTime()) { return curr; } return latest; }, - undefined as undefined | StoredDeployment, + undefined as undefined | Version, ); } 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 ec3ac9f0f..70703fd70 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 @@ -7,7 +7,7 @@ import { isUserErrorResponse } from '@/lib/user-error'; import { getProcessDeployments } from '@/lib/data/deployment'; async function Deployment({ processId, spaceId }: { processId: string; spaceId: string }) { - const deployments = await getProcessDeployments(spaceId, processId); + const deployments = await getProcessDeployments(spaceId, processId, undefined, true, true); if (isUserErrorResponse(deployments)) { return ( 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 416e762e1..541e02de0 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 @@ -26,7 +26,7 @@ import { ColorOptions, colorOptions } from './instance-coloring'; import { RemoveReadOnly, truthyFilter } from '@/lib/typescript-utils'; import type { ElementLike } from 'diagram-js/lib/core/Types'; import { wrapServerCall } from '@/lib/wrap-server-call'; -import { getLatestDeployment, getVersionInstances, getYoungestInstance } from './instance-helpers'; +import { getLatestVersion, getVersionInstances, getYoungestInstance } from './instance-helpers'; import useColors from './use-colors'; import useTokens from './use-tokens'; @@ -53,8 +53,8 @@ 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, pick } from '@/lib/helpers/javascriptHelpers'; -import { getProcessBPMN } from '@/lib/data/processes'; +import { asyncFilter, asyncMap, pick } from '@/lib/helpers/javascriptHelpers'; +import { getVersions } from '@/lib/data/processes'; import { enableInstanceCSVExport } from 'FeatureFlags'; import jsonToCsvExport from 'json-to-csv-export'; @@ -80,10 +80,40 @@ export default function ProcessDeploymentView({ processId }: { processId: string const canvasRef = useRef(null); const [infoPanelOpen, setInfoPanelOpen] = useState(false); + const isExecutableVersionMap = useRef>({}); + + const { data: versions } = useQuery({ + queryFn: async () => { + let versions = await getVersions(spaceId, processId); + if (isUserErrorResponse(versions)) return []; + + // filter out all versions that are not executable + versions = await asyncFilter(versions, async (version) => { + if (version.id in isExecutableVersionMap.current) { + return isExecutableVersionMap.current[version.id]; + } + + const bpmnObj = await toBpmnObject(version.bpmn); + const processes = getElementsByTagName(bpmnObj, 'bpmn:Process'); + if (!processes.length) return false; + isExecutableVersionMap.current[version.id] = processes[0].isExecutable; + return processes[0].isExecutable; + }); + + versions.sort((a, b) => { + return b.createdOn.getTime() - a.createdOn.getTime(); + }); + + return versions; + }, + queryKey: ['processVersions', spaceId, processId], + refetchInterval: 1000, + }); + // 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); + const deployments = await getProcessDeployments(spaceId, processId, undefined, true, true); if (isUserErrorResponse(deployments)) return null; return deployments; }, @@ -130,8 +160,8 @@ export default function ProcessDeploymentView({ processId }: { processId: string const { selectedVersion, versionInstances, currentVersion } = useMemo(() => { let selectedVersion, versionInstances, currentVersion; - if (deployments?.length) { - selectedVersion = deployments.find((d) => d.versionId === selectedVersionId)?.version; + if (versions?.length) { + selectedVersion = versions.find((v) => v.id === selectedVersionId); // sort instances newest first const rawInstances = getVersionInstances(knownInstances, selectedVersionId); @@ -143,13 +173,13 @@ export default function ProcessDeploymentView({ processId }: { processId: string ? versionInstances.find((i) => i.processInstanceId === selectedInstanceId) : undefined; - let currentVersionId = getLatestDeployment(deployments)!.versionId; + let currentVersionId = getLatestVersion(versions)!.id; if (selectedInstance) { currentVersionId = selectedInstance.processVersion; } else if (selectedVersionId) { currentVersionId = selectedVersionId; } - currentVersion = deployments.find((d) => d.versionId === currentVersionId)!.version; + currentVersion = versions.find((v) => v.id === currentVersionId); } return { @@ -157,7 +187,7 @@ export default function ProcessDeploymentView({ processId }: { processId: string versionInstances, currentVersion, }; - }, [deployments, knownInstances, selectedVersionId, selectedInstanceId]); + }, [versions, knownInstances, selectedVersionId, selectedInstanceId]); const { data: currentInstance, refetch: refetchCurrentInstance } = useQuery({ queryKey: ['processDeployments', spaceId, processId, 'instance', selectedInstanceId], @@ -179,9 +209,7 @@ export default function ProcessDeploymentView({ processId }: { processId: string const { data: selectedBpmn } = useQuery({ queryFn: async () => { - const bpmn = await getProcessBPMN(processId, spaceId, currentVersion?.id); - if (isUserErrorResponse(bpmn)) return undefined; - return { bpmn }; + return { bpmn: currentVersion?.bpmn || '' }; }, queryKey: ['space', spaceId, 'process', processId, 'version', currentVersion?.id || '', 'bpmn'], }); @@ -315,11 +343,11 @@ export default function ProcessDeploymentView({ processId }: { processId: string }, ] : []), - ...deployments.map(({ version }) => ({ - label: version.name, - key: `${version.id}`, + ...(versions?.map((v) => ({ + key: v.id, + label: v.name, disabled: false, - })), + })) || []), ], selectable: true, onSelect: ({ key }) => { @@ -370,14 +398,7 @@ export default function ProcessDeploymentView({ processId }: { processId: string setStartingInstance(true); await wrapServerCall({ fn: async () => { - const latestDeployment = getLatestDeployment(deployments); - if (!latestDeployment) { - return userError( - 'The current process does not seem to be deployed anymore.', - ); - } - - const { versionId } = latestDeployment; + const { id: versionId } = currentVersion!; let startForm = await getProcessStartForm(spaceId, processId, versionId); @@ -451,7 +472,7 @@ export default function ProcessDeploymentView({ processId }: { processId: string onClick={async () => { setTogglingActivation(true); const nextState = !isProcessActivated; - const versionId = getLatestDeployment(deployments)!.versionId; + const versionId = currentVersion!.id; await wrapServerCall({ fn: () => changeDeploymentActivation(processId, spaceId, versionId, nextState), @@ -641,10 +662,10 @@ export default function ProcessDeploymentView({ processId }: { processId: string // start the instance with the initial variable values from the start form await wrapServerCall({ fn: async () => { - const deployment = getLatestDeployment(deployments); - - if (!deployment) { - return userError('The current process does not seem to be deployed.'); + if (!currentVersion) { + return userError( + 'The current process does not seem to have versions to execute.', + ); } const mappedVariables: Record = {}; @@ -656,8 +677,8 @@ export default function ProcessDeploymentView({ processId }: { processId: string return startInstance( spaceId, - deployment.processId, - deployment.version.id, + currentVersion.processId, + currentVersion.id, mappedVariables, ); }, diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-list.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-list.tsx index 738e68f4a..077d50d50 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-list.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-list.tsx @@ -11,7 +11,7 @@ import processListStyles from '@/components/process-icon-list.module.scss'; type InputItem = { id: string; name: string; - versions: { id: string; name: string }[]; + versions: { id: string; name: string; deployed: boolean }[]; instances: string[]; }; export type DeployedProcessListProcess = ReplaceKeysWithHighlighted; @@ -95,9 +95,13 @@ const DeploymentsList = ({ dataIndex: 'id', key: 'Meta Data Button', title: '', - render: () => { + render: (_, record) => { return ( - ); @@ -138,6 +142,7 @@ const DeploymentsList = ({ style={{ float: 'right' }} type="text" onClick={() => removeDeployment(record.id)} + disabled={record.versions.every((v) => !v.deployed)} > diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx index 8f6f35ea5..a4d1d2b5c 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx @@ -1,7 +1,8 @@ 'use client'; -import { App, Button } from 'antd'; -import { useEffect, useState, useTransition } from 'react'; +import { App, Button, Checkbox, Space, Spin, Tooltip, Typography } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { useCallback, useEffect, useState, useTransition } from 'react'; import DeploymentsModal from './deployments-modal'; import Bar from '@/components/bar'; import useFuzySearch from '@/lib/useFuzySearch'; @@ -10,7 +11,7 @@ import { Folder } from '@/lib/data/folder-schema'; import { Process, ProcessMetadata } from '@/lib/data/process-schema'; import { useEnvironment } from '@/components/auth-can'; import { processUnchangedFromBasedOnVersion } from '@/lib/data/processes'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { deployProcess as serverDeployProcess, removeDeployment as serverRemoveDeployment, @@ -34,12 +35,13 @@ const DeploymentsView = ({ deployedProcesses: { id: string; name: string; - versions: { id: string; name: string }[]; + versions: { id: string; name: string; deployed: boolean }[]; instances: string[]; }[]; }) => { const [modalIsOpen, setModalIsOpen] = useState(false); const app = App.useApp(); + const pathname = usePathname(); const space = useEnvironment(); const router = useRouter(); const queryClient = useQueryClient(); @@ -51,6 +53,25 @@ const DeploymentsView = ({ transformData: (matches) => matches.map((match) => match.item), }); + const [togglingShowArchived, startTogglingShowArchived] = useTransition(); + const query = useSearchParams(); + // Get a new searchParams string by merging the current + // searchParams with a provided key/value pair + // see: https://nextjs.org/docs/app/api-reference/functions/use-search-params#updating-searchparams + const createQueryString = useCallback( + (name: string, value: string) => { + const params = new URLSearchParams(query.toString()); + if (!value) { + params.delete(name); + } else { + params.set(name, value); + } + + return params.toString(); + }, + [query], + ); + const [checkingProcessVersion, startCheckingProcessVersion] = useTransition(); function deployProcess( process: Pick, @@ -125,7 +146,8 @@ const DeploymentsView = ({ setInitialLoading(false); }, []); - const loading = initialLoading || checkingProcessVersion || removingDeployment; + const loading = + initialLoading || checkingProcessVersion || removingDeployment || togglingShowArchived; const tableProps: { loading: boolean; pagination?: false } = { loading }; @@ -133,7 +155,14 @@ const DeploymentsView = ({ return (
-
+
+ + + {togglingShowArchived ? ( + + ) : ( + + startTogglingShowArchived(() => { + router.push( + pathname + '?' + createQueryString('archived', el.target.checked ? 'true' : ''), + ); + }) + } + /> + )} + + Show Past Executions{' '} + + + + +
{ - return getDeployedProcesses(activeEnvironment.spaceId); + return getDeployedProcesses(activeEnvironment.spaceId, withArchived); })(), ]); @@ -33,7 +39,11 @@ async function Executions({ environmentId }: { environmentId: string }) { return { id: p.id, name: p.name, - versions: p.versions.map((v) => ({ id: v.id, name: v.name })), + versions: p.versions.map((v) => ({ + id: v.id, + name: v.name, + deployed: v.deployments.some((d) => !d.removeTime), + })), instances: p.versions.flatMap((v) => v.deployments.flatMap((d) => d.instances.map((i) => i.id)), ), @@ -52,11 +62,16 @@ async function Executions({ environmentId }: { environmentId: string }) { export default async function ExecutionsPage(props: { params: Promise<{ environmentId: string }>; + searchParams: Promise<{ archived: string }>; }) { const params = await props.params; + const searchParams = await props.searchParams; return ( - + ); } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx index 2f6633ce3..036a80c4b 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx @@ -149,7 +149,7 @@ const DashboardLayout = async ( key: 'start', label: Start, icon: , - selectedRegex: '/start($|/)', + selectedRegex: '/start($|/|\\?)', }); const automationSettings = await getSpaceSettingsValues( @@ -170,7 +170,7 @@ const DashboardLayout = async ( msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_TASK_EDITOR_ACTIVE && automationSettings.task_editor?.active !== false ) { - childRegex = '/tasks($|/)'; + childRegex = '/tasks($|/|\\?)'; children.push({ key: 'task-editor', label: Task Editor, @@ -203,7 +203,7 @@ const DashboardLayout = async ( /> ), - selectedRegex: '/tasklist($|/)', + selectedRegex: '/tasklist($|/|\\?)', openRegex: childRegex, children: children.length ? children : undefined, }); @@ -216,13 +216,13 @@ const DashboardLayout = async ( ); if (documentationSettings.active !== false) { - const processRegex = '/processes($|/)'; + const processRegex = '/processes($|/|\\?)'; let children: ExtendedMenuItems = [ documentationSettings.list?.active !== false && { key: 'processes-list', label: Process List, icon: , - selectedRegex: '/processes/list($|/)', + selectedRegex: '/processes/list($|/|\\?)', }, documentationSettings.editor?.active !== false && ability.can('manage', 'Process') && { @@ -231,7 +231,7 @@ const DashboardLayout = async ( Process Editor ), icon: , - selectedRegex: '/processes/editor($|/)', + selectedRegex: '/processes/editor($|/|\\?)', }, ].filter(truthyFilter); @@ -257,7 +257,7 @@ const DashboardLayout = async ( ability.can('view', 'Execution') && automationSettings.dashboard?.active !== false ) { - const dashboardRegex = '/executions-dashboard($|/)'; + const dashboardRegex = '/executions-dashboard($|/|\\?)'; childRegex = !childRegex ? dashboardRegex : `(${childRegex})|(${dashboardRegex})`; children.push({ key: 'dashboard', @@ -267,7 +267,7 @@ const DashboardLayout = async ( }); } if (ability.can('view', 'Execution') && automationSettings.executions?.active !== false) { - const executionsRegex = '/executions($|/)'; + const executionsRegex = '/executions($|/|\\?)'; childRegex = !childRegex ? executionsRegex : `(${childRegex})|(${executionsRegex})`; children.push({ key: 'executions', @@ -277,7 +277,7 @@ const DashboardLayout = async ( }); } if (ability.can('view', 'Machine') && automationSettings.machines?.active !== false) { - const machinesRegex = '/engines($|/)'; + const machinesRegex = '/engines($|/|\\?)'; childRegex = !childRegex ? machinesRegex : `(${childRegex})|(${machinesRegex})`; children.push({ key: 'machines', @@ -313,7 +313,7 @@ const DashboardLayout = async ( ), - selectedRegex: `/machine-config(?!/${userId}|/${activeEnvironment.spaceId})($|/)`, + selectedRegex: `/machine-config(?!/${userId}|/${activeEnvironment.spaceId})($|/|\\?)`, }); } @@ -329,7 +329,7 @@ const DashboardLayout = async ( let childRegex = ''; if (can('update', 'Environment') || can('delete', 'Environment')) { - const settingsRegex = '/settings($|/)'; + const settingsRegex = '/settings($|/|\\?)'; childRegex = !childRegex ? settingsRegex : `(${childRegex})|(${settingsRegex})`; children.push({ key: 'organization-settings', @@ -343,7 +343,7 @@ const DashboardLayout = async ( activeEnvironment.isOrganization && (can('update', 'Environment') || can('delete', 'Environment')) ) { - const managementRegex = '/management($|/)'; + const managementRegex = '/management($|/|\\?)'; childRegex = !childRegex ? managementRegex : `(${childRegex})|(${managementRegex})`; children.push({ key: 'organization-management', @@ -354,7 +354,7 @@ const DashboardLayout = async ( } // Data view under Organization - const dataRegex = `/machine-config/${activeEnvironment.spaceId}($|/)`; + const dataRegex = `/machine-config/${activeEnvironment.spaceId}($|/|\\?)`; childRegex = !childRegex ? dataRegex : `(${childRegex})|(${dataRegex})`; children.push({ key: 'organization-data', @@ -368,7 +368,7 @@ const DashboardLayout = async ( }); if (can('manage', 'User')) { - const userRegex = '/iam/users($|/)'; + const userRegex = '/iam/users($|/|\\?)'; childRegex = !childRegex ? userRegex : `(${childRegex})|(${userRegex})`; children.push({ key: 'users', @@ -379,7 +379,7 @@ const DashboardLayout = async ( } if (can('admin', 'All')) { - const rolesRegex = '/iam/roles($|/)'; + const rolesRegex = '/iam/roles($|/|\\?)'; childRegex = !childRegex ? rolesRegex : `(${childRegex})|(${rolesRegex})`; children.push({ key: 'roles', @@ -400,8 +400,8 @@ const DashboardLayout = async ( } if (msConfig.PROCEED_PUBLIC_IAM_ACTIVE) { - const profileRegex = '/profile($|/)'; - const spacesRegex = '/spaces($|/)'; + const profileRegex = '/profile($|/|\\?)'; + const spacesRegex = '/spaces($|/|\\?)'; // Only match if the url contains the source=personal query parameter const personalDataRegex = `/machine-config/${userId}\\?source=personal($|&)`; const regex = `(${profileRegex})|(${spacesRegex})|(${personalDataRegex})`; @@ -449,7 +449,7 @@ const DashboardLayout = async ( } if (!activeEnvironment.isOrganization) { - const settingsRegex = '/settings($|/)'; + const settingsRegex = '/settings($|/|\\?)'; // Match only the base path without query parameters, or with query params that don't include source=personal const personalHomeDataRegex = `/machine-config/${activeEnvironment.spaceId}(?!\\?source=personal)($|\\?)`; const regex = `(${settingsRegex})|(${personalHomeDataRegex})`; diff --git a/src/management-system-v2/lib/data/deployment.ts b/src/management-system-v2/lib/data/deployment.ts index d9ba04a73..6e6c2af7d 100644 --- a/src/management-system-v2/lib/data/deployment.ts +++ b/src/management-system-v2/lib/data/deployment.ts @@ -7,7 +7,7 @@ import { SuccessType, UserErrorType, userError } from '../user-error'; import Ability from '../ability/abilityHelper'; import { cacheLife, cacheTag, revalidateTag } from 'next/cache'; -export async function getDeployedProcesses(environmentId: string) { +export async function getDeployedProcesses(environmentId: string, withArchived = false) { const { ability } = await getCurrentEnvironment(environmentId); if (!ability.can('view', 'Execution')) @@ -17,32 +17,17 @@ export async function getDeployedProcesses(environmentId: string) { 'use cache'; cacheLife({ revalidate: 10, expire: 15 }); cacheTag(`space/${environmentId}/deployments`); - const deploymentIsNotDeleted = { removeTime: null }; return await db.process.findMany({ where: { - // get all processes in the current environment that have at least one version that is - // currently deployed - AND: [ - { environmentId }, - { NOT: { folderId: null } }, - { - versions: { - some: { deployments: { some: deploymentIsNotDeleted } }, - }, - }, - ], + AND: [{ environmentId }], }, select: { id: true, name: true, versions: { - // only include deployed versions in the output - where: { deployments: { some: deploymentIsNotDeleted } }, include: { deployments: { - // only include deployments in the output that are still actively deployed - where: deploymentIsNotDeleted, include: { instances: { select: { id: true } } }, }, }, @@ -51,7 +36,29 @@ export async function getDeployedProcesses(environmentId: string) { }); } - return ability.filter('view', 'Process', await getFromDBOrCache(environmentId)); + let deployedProcesses = await getFromDBOrCache(environmentId); + + deployedProcesses = deployedProcesses + .filter((p) => { + if (withArchived) { + return p.versions.some((v) => !!v.deployments.length); + } + + return p.versions.some((v) => v.deployments.filter((d) => !d.removeTime).length); + }) + .map((p) => { + return { + ...p, + versions: p.versions + .map((v) => ({ + ...v, + deployments: withArchived ? v.deployments : v.deployments.filter((d) => !d.removeTime), + })) + .filter((v) => !!v.deployments.length), + }; + }); + + return ability.filter('view', 'Process', deployedProcesses); } export async function getProcessDeployments( @@ -59,6 +66,7 @@ export async function getProcessDeployments( processId: string, ability?: Ability, showArchivedProcesses = false, + showArchivedDeployments = false, ) { if (!ability) ({ ability } = await getCurrentEnvironment(spaceId)); @@ -70,21 +78,15 @@ export async function getProcessDeployments( cacheLife({ revalidate: 10, expire: 15 }); cacheTag(`deployments/process/${processId}`); const deployments = await db.processDeployment.findMany({ - where: { - AND: [ - { - version: { - AND: [ - { processId }, - ...(!showArchivedProcesses ? [{ NOT: { process: { folderId: null } } }] : []), - ], - }, - }, - { removeTime: null }, - ], - }, include: { - version: { select: { id: true, processId: true, name: true } }, + version: { + select: { + id: true, + processId: true, + name: true, + process: { select: { folderId: true } }, + }, + }, instances: { select: { id: true } }, engine: { include: { @@ -106,7 +108,16 @@ export async function getProcessDeployments( })); } - return getFromDBOrCache(processId); + let deployments = await getFromDBOrCache(processId); + + deployments = deployments.filter((d) => { + if (!showArchivedProcesses && d.version.process.folderId === null) return false; + if (!showArchivedDeployments && d.removeTime !== null) return false; + + return true; + }); + + return deployments; } export type StoredDeployment = SuccessType< @@ -126,12 +137,14 @@ export async function addDeployment( const data = DeploymentInputSchema.parse(input); - await db.processDeployment.createMany({ + const res = await db.processDeployment.createManyAndReturn({ data: data.engineIds.map((engineId) => ({ ...data, engineIds: undefined, engineId })), }); revalidateTag(`space/${spaceId}/deployments`, 'max'); revalidateTag(`deployments/process/${processId}`, 'max'); + + return res; } export async function updateDeployment( diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index 95f4017df..47e19014c 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -38,7 +38,7 @@ import { // Antd uses barrel files, which next optimizes away. That requires us to import // antd components directly from their files in this server actions file. import { Process, ProcessMetadata } from './process-schema'; -import { revalidatePath } from 'next/cache'; +import { cacheLife, cacheTag, revalidatePath, updateTag } from 'next/cache'; import { getUsersFavourites } from './users'; import { checkIfProcessAlreadyExistsForAUserInASpaceByName, @@ -79,6 +79,7 @@ import { asyncMap } from '../helpers/javascriptHelpers'; import { isActive } from '@/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/instance-helpers'; import { InstanceInfo } from '../engines/deployment'; import { getMSConfig } from '../ms-config/ms-config'; +import { retrieveFile } from './file-manager/file-manager'; import { removeDeployment } from '../executions/deployment-server-actions'; // Import necessary functions from processModule @@ -627,6 +628,32 @@ export const copyProcesses = async ( return copiedProcesses; }; +export const getVersions = async (spaceId: string, processId: string) => { + const error = await checkValidity(processId, 'view', spaceId); + + if (error) return error; + + async function getFromDBOrCache(processId: string) { + 'use cache'; + cacheLife({ revalidate: 60, expire: 120 }); + cacheTag(`/process/${processId}/versions`); + const versions = await db.version.findMany({ + where: { processId }, + }); + + return asyncMap(versions, async (version) => { + const bpmn = ((await retrieveFile(version.bpmnFilePath, false)) as Buffer).toString('utf8'); + + return { + ...version, + bpmn, + }; + }); + } + + return await getFromDBOrCache(processId); +}; + /** * Function that checks if a process' latest version is unchanged from the version it is based on * @@ -715,6 +742,8 @@ export const createVersion = async ( await updateProcessVersionBasedOn({ ...process, bpmn }, versionId); + updateTag(`/process/${processId}/versions`); + if (basedOnBPMN) { const config = await getMSConfig(); if (config.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) { diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index f765540ad..9bda2f625 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -34,7 +34,7 @@ export async function getUserTasks(spaceId: string, ability?: Ability) { { instance: { deployment: { - AND: [{ version: { process: { environmentId } } }, { removeTime: null }], + AND: [{ version: { process: { environmentId } } }], }, }, }, diff --git a/src/management-system-v2/lib/executions/deployment-server-actions.ts b/src/management-system-v2/lib/executions/deployment-server-actions.ts index b48237ab5..13bdb8aa0 100644 --- a/src/management-system-v2/lib/executions/deployment-server-actions.ts +++ b/src/management-system-v2/lib/executions/deployment-server-actions.ts @@ -112,7 +112,7 @@ export async function deployProcess( ability, ); - await addDeployment( + const newDeployments = await addDeployment( spaceId, definitionId, { @@ -125,6 +125,8 @@ export async function deployProcess( ability, ); + if (isUserErrorResponse(newDeployments)) return newDeployments; + // deactivate the process on all engines that have a deployment but which were not target of the // new deployment await Promise.allSettled( @@ -139,6 +141,11 @@ export async function deployProcess( } }), ); + + return newDeployments.map((d) => ({ + ...d, + engine: deployedTo.find((e) => e.id === d.engineId)!, + })); } catch (e) { const message = getErrorMessage(e); return userError(message); diff --git a/src/management-system-v2/lib/executions/instance-server-actions.ts b/src/management-system-v2/lib/executions/instance-server-actions.ts index 25a9252f6..8d89433b7 100644 --- a/src/management-system-v2/lib/executions/instance-server-actions.ts +++ b/src/management-system-v2/lib/executions/instance-server-actions.ts @@ -3,6 +3,7 @@ import { asyncForEach, asyncMap } from '@/lib/helpers/javascriptHelpers'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { + UserError, UserErrorType, getErrorMessage, isUserErrorResponse, @@ -37,6 +38,7 @@ import { truthyFilter } from '@/lib/typescript-utils'; import { getInstanceFile, saveInstanceArtifact } from '../data/file-manager-facade'; import { getProcessVersion } from '../data/db/process'; import Ability from '../ability/abilityHelper'; +import { deployProcess } from './deployment-server-actions'; export async function getProcessStartForm( spaceId: string, @@ -68,7 +70,8 @@ export async function startInstance( if (!ability.can('create', 'Execution')) return userError('Invalid Permissions', UserErrorType.PermissionError); - const deployments = await getProcessDeployments(spaceId, definitionId, ability); + let deployments: { id: string; versionId: string; engine: Engine }[] | { error: UserError } = + await getProcessDeployments(spaceId, definitionId, ability); if (isUserErrorResponse(deployments)) return deployments; if (!userId) ({ userId } = await getCurrentUser()); @@ -77,8 +80,23 @@ export async function startInstance( return d.versionId === versionId; }); - // TODO: automatically deploy the version if possible - if (!versionDeployments.length) return userError('This process version is not deployed.'); + if (!versionDeployments.length) { + const res = await deployProcess( + definitionId, + versionId, + spaceId, + 'dynamic', + undefined, + ability, + userId, + ); + + if (isUserErrorResponse(res)) return res; + + if (!res) return userError('Failed to start an instance'); + + deployments = res.map((d) => ({ ...d })); + } for (const deployment of deployments) { const { engine } = deployment; diff --git a/src/management-system-v2/mcp-tools/startProcess.ts b/src/management-system-v2/mcp-tools/startProcess.ts index 072599c3d..f284cec58 100644 --- a/src/management-system-v2/mcp-tools/startProcess.ts +++ b/src/management-system-v2/mcp-tools/startProcess.ts @@ -70,18 +70,6 @@ export default async function startProcess({ // we don't need to check if the variables that are required at startup are set since the engine // will do that for us and return an error if they aren't - const deployment = await deployProcess( - processId, - process.version.id, - environmentId, - 'dynamic', - undefined, - ability, - userId, - ); - - if (isUserErrorResponse(deployment)) return deployment.error.message; - const instanceId = await startInstance( environmentId, processId,