diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index b44b9337a7512..712927de97fd1 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -16,11 +16,11 @@ describe('Current Workflow Executions', () => { it('should render executions tab correctly', () => { createMockExecutions(); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions'); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionListItems().should('have.length', 11); executionsTab.getters.successfulExecutionListItems().should('have.length', 9); @@ -34,7 +34,7 @@ describe('Current Workflow Executions', () => { it('should not redirect back to execution tab when request is not done before leaving the page', () => { cy.intercept('GET', '/rest/executions?filter=*'); - cy.intercept('GET', '/rest/executions-current?filter=*'); + cy.intercept('GET', '/rest/executions/active?filter=*'); executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToEditorTab(); @@ -63,7 +63,7 @@ describe('Current Workflow Executions', () => { }; cy.intercept('GET', '/rest/executions?filter=*', throttleResponse); - cy.intercept('GET', '/rest/executions-current?filter=*', throttleResponse); + cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse); executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToEditorTab(); diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index 699f07d53ff63..b022ce5ac6333 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -19,7 +19,7 @@ describe('Debug', () => { it('should be able to debug executions', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions/*').as('getExecution'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions'); cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); @@ -41,7 +41,7 @@ describe('Debug', () => { executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click(); cy.url().should('include', '/debug'); @@ -66,7 +66,7 @@ describe('Debug', () => { executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionListItems().should('have.length', 2).first().click(); cy.wait(['@getExecution']); @@ -77,7 +77,7 @@ describe('Debug', () => { confirmDialog.find('li').should('have.length', 2); confirmDialog.get('.btn--cancel').click(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionListItems().should('have.length', 2).first().click(); cy.wait(['@getExecution']); @@ -108,7 +108,7 @@ describe('Debug', () => { cy.url().should('not.include', '/debug'); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); @@ -130,7 +130,7 @@ describe('Debug', () => { workflowPage.actions.deleteNode(IF_NODE_NAME); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionListItems().should('have.length', 3).first().click(); cy.wait(['@getExecution']); executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); diff --git a/cypress/e2e/30-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index 656d7e9b781c3..733753314b5ff 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -136,10 +136,10 @@ describe('Editor actions should work', () => { it('after switching between Editor and Executions', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions'); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); cy.wait(500); executionsTab.actions.switchToEditorTab(); editWorkflowAndDeactivate(); @@ -149,7 +149,7 @@ describe('Editor actions should work', () => { it('after switching between Editor and Debug', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions/*').as('getExecution'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions'); cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); editWorkflowAndDeactivate(); @@ -157,7 +157,7 @@ describe('Editor actions should work', () => { cy.wait(['@postWorkflowRun']); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); executionsTab.getters.executionListItems().should('have.length', 1).first().click(); cy.wait(['@getExecution']); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index ef15dd97d7f0c..096fae738dd3c 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -259,7 +259,7 @@ describe('Workflow Actions', () => { it('should keep endpoint click working when switching between execution and editor tab', () => { cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions'); WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); @@ -270,7 +270,7 @@ describe('Workflow Actions', () => { cy.get('body').type('{esc}'); executionsTab.actions.switchToExecutionsTab(); - cy.wait(['@getExecutions', '@getCurrentExecutions']); + cy.wait(['@getExecutions', '@getActiveExecutions']); cy.wait(500); executionsTab.actions.switchToEditorTab(); diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 5dcf0272f92c2..9c2fab9c3150c 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -18,7 +18,7 @@ import type { Workflow, WorkflowExecuteMode, ExecutionStatus, - IExecutionsSummary, + ExecutionSummary, FeatureFlags, INodeProperties, IUserSettings, @@ -170,8 +170,7 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted { export interface IExecutionsListResponse { count: number; - // results: IExecutionShortResponse[]; - results: IExecutionsSummary[]; + results: ExecutionSummary[]; estimated: boolean; } @@ -192,12 +191,6 @@ export interface IExecutionsCurrentSummary { status?: ExecutionStatus; } -export interface IExecutionDeleteFilter { - deleteBefore?: Date; - filters?: IDataObject; - ids?: string[]; -} - export interface IExecutingWorkflowData { executionData: IWorkflowExecutionDataProcess; process?: ChildProcess; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index b23225e4dcbfc..792dfd28ba0af 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -14,13 +14,10 @@ import cookieParser from 'cookie-parser'; import express from 'express'; import { engine as expressHandlebars } from 'express-handlebars'; import type { ServeStaticOptions } from 'serve-static'; -import type { FindManyOptions, FindOptionsWhere } from 'typeorm'; -import { Not, In } from 'typeorm'; import { type Class, InstanceSettings } from 'n8n-core'; -import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow'; -import { jsonParse } from 'n8n-workflow'; +import type { IN8nUISettings } from 'n8n-workflow'; // @ts-ignore import timezones from 'google-timezones-json'; @@ -39,7 +36,6 @@ import { } from '@/constants'; import { credentialsController } from '@/credentials/credentials.controller'; import type { CurlHelper } from '@/requests'; -import type { ExecutionRequest } from '@/executions/execution.request'; import { registerController } from '@/decorators'; import { AuthController } from '@/controllers/auth.controller'; import { BinaryDataController } from '@/controllers/binaryData.controller'; @@ -58,7 +54,7 @@ import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.c import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee'; import { ExecutionsController } from '@/executions/executions.controller'; import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi'; -import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces'; +import type { ICredentialsOverwrite, IDiagnosticInfo } from '@/Interfaces'; import { ActiveExecutions } from '@/ActiveExecutions'; import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; @@ -76,7 +72,6 @@ import { PostHogClient } from './posthog'; import { eventBus } from './eventbus'; import { InternalHooks } from './InternalHooks'; import { License } from './License'; -import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers'; import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlService } from './sso/saml/saml.service.ee'; import { VariablesController } from './environments/variables/variables.controller.ee'; @@ -87,8 +82,6 @@ import { import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee'; import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee'; -import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; -import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; @@ -100,9 +93,7 @@ import { InvitationController } from './controllers/invitation.controller'; import { CollaborationService } from './collaboration/collaboration.service'; import { RoleController } from './controllers/role.controller'; import { BadRequestError } from './errors/response-errors/bad-request.error'; -import { NotFoundError } from './errors/response-errors/not-found.error'; import { OrchestrationService } from '@/services/orchestration.service'; -import { WorkflowSharingService } from './workflows/workflowSharing.service'; const exec = promisify(callbackExec); @@ -408,219 +399,6 @@ export class Server extends AbstractServer { }), ); - // ---------------------------------------- - // Executing Workflows - // ---------------------------------------- - - // Returns all the currently working executions - this.app.get( - `/${this.restEndpoint}/executions-current`, - ResponseHelper.send( - async (req: ExecutionRequest.GetAllCurrent): Promise => { - if (config.getEnv('executions.mode') === 'queue') { - const queue = Container.get(Queue); - const currentJobs = await queue.getJobs(['active', 'waiting']); - - const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId); - - const currentlyRunningManualExecutions = - this.activeExecutionsInstance.getActiveExecutions(); - const manualExecutionIds = currentlyRunningManualExecutions.map( - (execution) => execution.id, - ); - - const currentlyRunningExecutionIds = - currentlyRunningQueueIds.concat(manualExecutionIds); - - if (!currentlyRunningExecutionIds.length) return []; - - const findOptions: FindManyOptions & { - where: FindOptionsWhere; - } = { - select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt', 'stoppedAt', 'status'], - order: { id: 'DESC' }, - where: { - id: In(currentlyRunningExecutionIds), - status: Not(In(['finished', 'stopped', 'failed', 'crashed'] as ExecutionStatus[])), - }, - }; - - const sharedWorkflowIds = await Container.get( - WorkflowSharingService, - ).getSharedWorkflowIds(req.user); - - if (!sharedWorkflowIds.length) return []; - - if (req.query.filter) { - const { workflowId, status, finished } = jsonParse(req.query.filter); - if (workflowId && sharedWorkflowIds.includes(workflowId)) { - Object.assign(findOptions.where, { workflowId }); - } else { - Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) }); - } - if (status) { - Object.assign(findOptions.where, { status: In(status) }); - } - if (finished) { - Object.assign(findOptions.where, { finished }); - } - } else { - Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) }); - } - - const executions = - await Container.get(ExecutionRepository).findMultipleExecutions(findOptions); - - if (!executions.length) return []; - - return executions.map((execution) => { - if (!execution.status) { - execution.status = getStatusUsingPreviousExecutionStatusMethod(execution); - } - return { - id: execution.id, - workflowId: execution.workflowId, - mode: execution.mode, - retryOf: execution.retryOf !== null ? execution.retryOf : undefined, - startedAt: new Date(execution.startedAt), - status: execution.status ?? null, - stoppedAt: execution.stoppedAt ?? null, - } as IExecutionsSummary; - }); - } - - const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); - - const returnData: IExecutionsSummary[] = []; - - const filter = req.query.filter ? jsonParse(req.query.filter) : {}; - - const sharedWorkflowIds = await Container.get( - WorkflowSharingService, - ).getSharedWorkflowIds(req.user); - - for (const data of executingWorkflows) { - if ( - (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) || - (data.workflowId !== undefined && !sharedWorkflowIds.includes(data.workflowId)) - ) { - continue; - } - - returnData.push({ - id: data.id, - workflowId: data.workflowId === undefined ? '' : data.workflowId, - mode: data.mode, - retryOf: data.retryOf, - startedAt: new Date(data.startedAt), - status: data.status, - }); - } - - returnData.sort((a, b) => Number(b.id) - Number(a.id)); - - return returnData; - }, - ), - ); - - // Forces the execution to stop - this.app.post( - `/${this.restEndpoint}/executions-current/:id/stop`, - ResponseHelper.send(async (req: ExecutionRequest.Stop): Promise => { - const { id: executionId } = req.params; - - const sharedWorkflowIds = await Container.get(WorkflowSharingService).getSharedWorkflowIds( - req.user, - ); - - if (!sharedWorkflowIds.length) { - throw new NotFoundError('Execution not found'); - } - - const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution( - executionId, - { - where: { - workflowId: In(sharedWorkflowIds), - }, - }, - ); - - if (!fullExecutionData) { - throw new NotFoundError('Execution not found'); - } - - if (config.getEnv('executions.mode') === 'queue') { - // Manual executions should still be stoppable, so - // try notifying the `activeExecutions` to stop it. - const result = await this.activeExecutionsInstance.stopExecution(req.params.id); - - if (result === undefined) { - // If active execution could not be found check if it is a waiting one - try { - return await this.waitTracker.stopExecution(req.params.id); - } catch (error) { - // Ignore, if it errors as then it is probably a currently running - // execution - } - } else { - return { - mode: result.mode, - startedAt: new Date(result.startedAt), - stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, - finished: result.finished, - status: result.status, - } as IExecutionsStopData; - } - - const queue = Container.get(Queue); - const currentJobs = await queue.getJobs(['active', 'waiting']); - - const job = currentJobs.find((job) => job.data.executionId === req.params.id); - - if (!job) { - this.logger.debug('Could not stop job because it is no longer in queue', { - jobId: req.params.id, - }); - } else { - await queue.stopJob(job); - } - - const returnData: IExecutionsStopData = { - mode: fullExecutionData.mode, - startedAt: new Date(fullExecutionData.startedAt), - stoppedAt: fullExecutionData.stoppedAt - ? new Date(fullExecutionData.stoppedAt) - : undefined, - finished: fullExecutionData.finished, - status: fullExecutionData.status, - }; - - return returnData; - } - - // Stop the execution and wait till it is done and we got the data - const result = await this.activeExecutionsInstance.stopExecution(executionId); - - let returnData: IExecutionsStopData; - if (result === undefined) { - // If active execution could not be found check if it is a waiting one - returnData = await this.waitTracker.stopExecution(executionId); - } else { - returnData = { - mode: result.mode, - startedAt: new Date(result.startedAt), - stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, - finished: result.finished, - status: result.status, - }; - } - - return returnData; - }), - ); - // ---------------------------------------- // Options // ---------------------------------------- diff --git a/packages/cli/src/controllers/workflow-statistics.types.ts b/packages/cli/src/controllers/workflow-statistics.types.ts new file mode 100644 index 0000000000000..124f48f2529b4 --- /dev/null +++ b/packages/cli/src/controllers/workflow-statistics.types.ts @@ -0,0 +1,5 @@ +import type { ExecutionRequest } from '@/executions/execution.types'; + +export namespace StatisticsRequest { + export type GetOne = ExecutionRequest.GetOne; +} diff --git a/packages/cli/src/controllers/workflowStatistics.controller.ts b/packages/cli/src/controllers/workflowStatistics.controller.ts index b36661f222846..caa9f3cae3c84 100644 --- a/packages/cli/src/controllers/workflowStatistics.controller.ts +++ b/packages/cli/src/controllers/workflowStatistics.controller.ts @@ -4,10 +4,10 @@ import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import { StatisticsNames } from '@db/entities/WorkflowStatistics'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; -import { ExecutionRequest } from '@/executions/execution.request'; import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces'; import { Logger } from '@/Logger'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { StatisticsRequest } from './workflow-statistics.types'; interface WorkflowStatisticsData { productionSuccess: T; @@ -29,7 +29,7 @@ export class WorkflowStatisticsController { */ // TODO: move this into a new decorator `@ValidateWorkflowPermission` @Middleware() - async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) { + async hasWorkflowAccess(req: StatisticsRequest.GetOne, res: Response, next: NextFunction) { const { user } = req; const workflowId = req.params.id; @@ -48,17 +48,17 @@ export class WorkflowStatisticsController { } @Get('/:id/counts/') - async getCounts(req: ExecutionRequest.Get): Promise> { + async getCounts(req: StatisticsRequest.GetOne): Promise> { return await this.getData(req.params.id, 'count', 0); } @Get('/:id/times/') - async getTimes(req: ExecutionRequest.Get): Promise> { + async getTimes(req: StatisticsRequest.GetOne): Promise> { return await this.getData(req.params.id, 'latestEvent', null); } @Get('/:id/data-loaded/') - async getDataLoaded(req: ExecutionRequest.Get): Promise { + async getDataLoaded(req: StatisticsRequest.GetOne): Promise { // Get flag const workflowId = req.params.id; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 4aa94548f171f..3f17767d0f7d3 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -23,7 +23,7 @@ import { parse, stringify } from 'flatted'; import { ApplicationError, type ExecutionStatus, - type IExecutionsSummary, + type ExecutionSummary, type IRunExecutionData, } from 'n8n-workflow'; import { BinaryDataService } from 'n8n-core'; @@ -41,6 +41,7 @@ import { ExecutionEntity } from '../entities/ExecutionEntity'; import { ExecutionMetadata } from '../entities/ExecutionMetadata'; import { ExecutionDataRepository } from './executionData.repository'; import { Logger } from '@/Logger'; +import type { GetManyActiveFilter } from '@/executions/execution.types'; function parseFiltersToQueryBuilder( qb: SelectQueryBuilder, @@ -343,7 +344,7 @@ export class ExecutionRepository extends Repository { excludedExecutionIds: string[], accessibleWorkflowIds: string[], additionalFilters?: { lastId?: string; firstId?: string }, - ): Promise { + ): Promise { if (accessibleWorkflowIds.length === 0) { return []; } @@ -657,6 +658,47 @@ export class ExecutionRepository extends Repository { unflattenData: false, }); } + + async findIfAccessible(executionId: string, accessibleWorkflowIds: string[]) { + return await this.findSingleExecution(executionId, { + where: { workflowId: In(accessibleWorkflowIds) }, + }); + } + + async getManyActive( + activeExecutionIds: string[], + accessibleWorkflowIds: string[], + filter?: GetManyActiveFilter, + ) { + const where: FindOptionsWhere = { + id: In(activeExecutionIds), + status: Not(In(['finished', 'stopped', 'failed', 'crashed'] as ExecutionStatus[])), + }; + + if (filter) { + const { workflowId, status, finished } = filter; + if (workflowId && accessibleWorkflowIds.includes(workflowId)) { + where.workflowId = workflowId; + } else { + where.workflowId = In(accessibleWorkflowIds); + } + if (status) { + // @ts-ignore + where.status = In(status); + } + if (finished !== undefined) { + where.finished = finished; + } + } else { + where.workflowId = In(accessibleWorkflowIds); + } + + return await this.findMultipleExecutions({ + select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt', 'stoppedAt', 'status'], + order: { id: 'DESC' }, + where, + }); + } } export interface IGetExecutionsQueryFilter { diff --git a/packages/cli/src/executions/active-execution.service.ts b/packages/cli/src/executions/active-execution.service.ts new file mode 100644 index 0000000000000..335c0a7faf661 --- /dev/null +++ b/packages/cli/src/executions/active-execution.service.ts @@ -0,0 +1,134 @@ +import { Service } from 'typedi'; +import { ActiveExecutions } from '@/ActiveExecutions'; +import { Logger } from '@/Logger'; +import { Queue } from '@/Queue'; +import { WaitTracker } from '@/WaitTracker'; +import { ExecutionRepository } from '@db/repositories/execution.repository'; +import { getStatusUsingPreviousExecutionStatusMethod } from '@/executions/executionHelpers'; +import config from '@/config'; + +import type { ExecutionSummary } from 'n8n-workflow'; +import type { IExecutionBase, IExecutionsCurrentSummary } from '@/Interfaces'; +import type { GetManyActiveFilter } from './execution.types'; + +@Service() +export class ActiveExecutionService { + constructor( + private readonly logger: Logger, + private readonly queue: Queue, + private readonly activeExecutions: ActiveExecutions, + private readonly executionRepository: ExecutionRepository, + private readonly waitTracker: WaitTracker, + ) {} + + private readonly isRegularMode = config.getEnv('executions.mode') === 'regular'; + + async findOne(executionId: string, accessibleWorkflowIds: string[]) { + return await this.executionRepository.findIfAccessible(executionId, accessibleWorkflowIds); + } + + private toSummary(execution: IExecutionsCurrentSummary | IExecutionBase): ExecutionSummary { + return { + id: execution.id, + workflowId: execution.workflowId ?? '', + mode: execution.mode, + retryOf: execution.retryOf !== null ? execution.retryOf : undefined, + startedAt: new Date(execution.startedAt), + status: execution.status, + stoppedAt: 'stoppedAt' in execution ? execution.stoppedAt : undefined, + }; + } + + // ---------------------------------- + // regular mode + // ---------------------------------- + + async findManyInRegularMode( + filter: GetManyActiveFilter, + accessibleWorkflowIds: string[], + ): Promise { + return this.activeExecutions + .getActiveExecutions() + .filter(({ workflowId }) => { + if (filter.workflowId && filter.workflowId !== workflowId) return false; + if (workflowId && !accessibleWorkflowIds.includes(workflowId)) return false; + return true; + }) + .map((execution) => this.toSummary(execution)) + .sort((a, b) => Number(b.id) - Number(a.id)); + } + + // ---------------------------------- + // queue mode + // ---------------------------------- + + async findManyInQueueMode(filter: GetManyActiveFilter, accessibleWorkflowIds: string[]) { + const activeManualExecutionIds = this.activeExecutions + .getActiveExecutions() + .map((execution) => execution.id); + + const activeJobs = await this.queue.getJobs(['active', 'waiting']); + + const activeProductionExecutionIds = activeJobs.map((job) => job.data.executionId); + + const activeExecutionIds = activeProductionExecutionIds.concat(activeManualExecutionIds); + + if (activeExecutionIds.length === 0) return []; + + const activeExecutions = await this.executionRepository.getManyActive( + activeExecutionIds, + accessibleWorkflowIds, + filter, + ); + + return activeExecutions.map((execution) => { + if (!execution.status) { + // @tech-debt Status should never be nullish + execution.status = getStatusUsingPreviousExecutionStatusMethod(execution); + } + + return this.toSummary(execution); + }); + } + + async stop(execution: IExecutionBase) { + const result = await this.activeExecutions.stopExecution(execution.id); + + if (result) { + return { + mode: result.mode, + startedAt: new Date(result.startedAt), + stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, + finished: result.finished, + status: result.status, + }; + } + + if (!this.isRegularMode) return await this.waitTracker.stopExecution(execution.id); + + // queue mode + + try { + return await this.waitTracker.stopExecution(execution.id); + } catch {} + + const activeJobs = await this.queue.getJobs(['active', 'waiting']); + const job = activeJobs.find(({ data }) => data.executionId === execution.id); + + if (!job) { + this.logger.debug('Could not stop job because it is no longer in queue', { + jobId: execution.id, + }); + } else { + await this.queue.stopJob(job); + } + + return { + mode: execution.mode, + startedAt: new Date(execution.startedAt), + stoppedAt: execution.stoppedAt ? new Date(execution.stoppedAt) : undefined, + finished: execution.finished, + status: execution.status, + }; + } +} diff --git a/packages/cli/src/executions/execution.request.ts b/packages/cli/src/executions/execution.request.ts deleted file mode 100644 index 96e1a058ae30b..0000000000000 --- a/packages/cli/src/executions/execution.request.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { IExecutionDeleteFilter } from '@/Interfaces'; -import type { AuthenticatedRequest } from '@/requests'; - -export declare namespace ExecutionRequest { - namespace QueryParam { - type GetAll = { - filter: string; // '{ waitTill: string; finished: boolean, [other: string]: string }' - limit: string; - lastId: string; - firstId: string; - }; - - type GetAllCurrent = { - filter: string; // '{ workflowId: string }' - }; - } - - type GetAll = AuthenticatedRequest<{}, {}, {}, QueryParam.GetAll>; - - type Get = AuthenticatedRequest<{ id: string }, {}, {}, { unflattedResponse: 'true' | 'false' }>; - - type Delete = AuthenticatedRequest<{}, {}, IExecutionDeleteFilter>; - - type Retry = AuthenticatedRequest<{ id: string }, {}, { loadWorkflow: boolean }, {}>; - - type Stop = AuthenticatedRequest<{ id: string }>; - - type GetAllCurrent = AuthenticatedRequest<{}, {}, {}, QueryParam.GetAllCurrent>; -} diff --git a/packages/cli/src/executions/execution.service.ee.ts b/packages/cli/src/executions/execution.service.ee.ts index 4b5ab8989299a..7dd65c682ff02 100644 --- a/packages/cli/src/executions/execution.service.ee.ts +++ b/packages/cli/src/executions/execution.service.ee.ts @@ -1,5 +1,5 @@ import { ExecutionService } from './execution.service'; -import type { ExecutionRequest } from './execution.request'; +import type { ExecutionRequest } from './execution.types'; import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces'; import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee'; import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types'; @@ -14,11 +14,11 @@ export class EnterpriseExecutionsService { private readonly enterpriseWorkflowService: EnterpriseWorkflowService, ) {} - async getExecution( - req: ExecutionRequest.Get, + async findOne( + req: ExecutionRequest.GetOne, sharedWorkflowIds: string[], ): Promise { - const execution = await this.executionService.getExecution(req, sharedWorkflowIds); + const execution = await this.executionService.findOne(req, sharedWorkflowIds); if (!execution) return; diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index c64b3953d0625..8f92538fa49ae 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -21,7 +21,7 @@ import type { } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; import { Queue } from '@/Queue'; -import type { ExecutionRequest } from './execution.request'; +import type { ExecutionRequest } from './execution.types'; import { WorkflowRunner } from '@/WorkflowRunner'; import * as GenericHelpers from '@/GenericHelpers'; import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers'; @@ -78,15 +78,7 @@ export class ExecutionService { private readonly nodeTypes: NodeTypes, ) {} - async getExecutionsList(req: ExecutionRequest.GetAll, sharedWorkflowIds: string[]) { - if (sharedWorkflowIds.length === 0) { - return { - count: 0, - estimated: false, - results: [], - }; - } - + async findMany(req: ExecutionRequest.GetMany, sharedWorkflowIds: string[]) { // parse incoming filter object and remove non-valid fields let filter: IGetExecutionsQueryFilter | undefined = undefined; if (req.query.filter) { @@ -160,8 +152,8 @@ export class ExecutionService { }; } - async getExecution( - req: ExecutionRequest.Get, + async findOne( + req: ExecutionRequest.GetOne, sharedWorkflowIds: string[], ): Promise { if (!sharedWorkflowIds.length) return undefined; @@ -184,9 +176,7 @@ export class ExecutionService { return execution; } - async retryExecution(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) { - if (!sharedWorkflowIds.length) return false; - + async retry(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) { const { id: executionId } = req.params; const execution = (await this.executionRepository.findIfShared( executionId, @@ -298,12 +288,7 @@ export class ExecutionService { return !!executionData.finished; } - async deleteExecutions(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) { - if (sharedWorkflowIds.length === 0) { - // return early since without shared workflows there can be no hits - // (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners) - return; - } + async delete(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) { const { deleteBefore, ids, filters: requestFiltersRaw } = req.body; let requestFilters; if (requestFiltersRaw) { diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts new file mode 100644 index 0000000000000..3ad21ae357f36 --- /dev/null +++ b/packages/cli/src/executions/execution.types.ts @@ -0,0 +1,48 @@ +import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity'; +import type { AuthenticatedRequest } from '@/requests'; +import type { ExecutionStatus, IDataObject } from 'n8n-workflow'; + +export declare namespace ExecutionRequest { + namespace QueryParams { + type GetMany = { + filter: string; // '{ waitTill: string; finished: boolean, [other: string]: string }' + limit: string; + lastId: string; + firstId: string; + }; + + type GetOne = { unflattedResponse: 'true' | 'false' }; + } + + namespace BodyParams { + type DeleteFilter = { + deleteBefore?: Date; + filters?: IDataObject; + ids?: string[]; + }; + } + + namespace RouteParams { + type ExecutionId = { + id: ExecutionEntity['id']; + }; + } + + type GetMany = AuthenticatedRequest<{}, {}, {}, QueryParams.GetMany>; + + type GetOne = AuthenticatedRequest; + + type Delete = AuthenticatedRequest<{}, {}, BodyParams.DeleteFilter>; + + type Retry = AuthenticatedRequest; + + type Stop = AuthenticatedRequest; + + type GetManyActive = AuthenticatedRequest<{}, {}, {}, { filter?: string }>; +} + +export type GetManyActiveFilter = { + workflowId?: string; + status?: ExecutionStatus; + finished?: boolean; +}; diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 5a0d4c40af105..699ce509d2f90 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -1,18 +1,26 @@ -import { ExecutionRequest } from './execution.request'; +import type { GetManyActiveFilter } from './execution.types'; +import { ExecutionRequest } from './execution.types'; import { ExecutionService } from './execution.service'; import { Authorized, Get, Post, RestController } from '@/decorators'; import { EnterpriseExecutionsService } from './execution.service.ee'; import { isSharingEnabled } from '@/UserManagement/UserManagementHelper'; import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; import type { User } from '@/databases/entities/User'; +import config from '@/config'; +import { jsonParse } from 'n8n-workflow'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { ActiveExecutionService } from './active-execution.service'; @Authorized() @RestController('/executions') export class ExecutionsController { + private readonly isQueueMode = config.getEnv('executions.mode') === 'queue'; + constructor( private readonly executionService: ExecutionService, private readonly enterpriseExecutionService: EnterpriseExecutionsService, private readonly workflowSharingService: WorkflowSharingService, + private readonly activeExecutionService: ActiveExecutionService, ) {} private async getAccessibleWorkflowIds(user: User) { @@ -22,32 +30,64 @@ export class ExecutionsController { } @Get('/') - async getExecutionsList(req: ExecutionRequest.GetAll) { + async getMany(req: ExecutionRequest.GetMany) { const workflowIds = await this.getAccessibleWorkflowIds(req.user); - return await this.executionService.getExecutionsList(req, workflowIds); + if (workflowIds.length === 0) return { count: 0, estimated: false, results: [] }; + + return await this.executionService.findMany(req, workflowIds); + } + + @Get('/active') + async getActive(req: ExecutionRequest.GetManyActive) { + const filter = req.query.filter?.length ? jsonParse(req.query.filter) : {}; + + const workflowIds = await this.getAccessibleWorkflowIds(req.user); + + return this.isQueueMode + ? await this.activeExecutionService.findManyInQueueMode(filter, workflowIds) + : await this.activeExecutionService.findManyInRegularMode(filter, workflowIds); + } + + @Post('/active/:id/stop') + async stop(req: ExecutionRequest.Stop) { + const workflowIds = await this.getAccessibleWorkflowIds(req.user); + + if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); + + const execution = await this.activeExecutionService.findOne(req.params.id, workflowIds); + + if (!execution) throw new NotFoundError('Execution not found'); + + return await this.activeExecutionService.stop(execution); } @Get('/:id') - async getExecution(req: ExecutionRequest.Get) { + async getOne(req: ExecutionRequest.GetOne) { const workflowIds = await this.getAccessibleWorkflowIds(req.user); + if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); + return isSharingEnabled() - ? await this.enterpriseExecutionService.getExecution(req, workflowIds) - : await this.executionService.getExecution(req, workflowIds); + ? await this.enterpriseExecutionService.findOne(req, workflowIds) + : await this.executionService.findOne(req, workflowIds); } @Post('/:id/retry') - async retryExecution(req: ExecutionRequest.Retry) { + async retry(req: ExecutionRequest.Retry) { const workflowIds = await this.getAccessibleWorkflowIds(req.user); - return await this.executionService.retryExecution(req, workflowIds); + if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); + + return await this.executionService.retry(req, workflowIds); } @Post('/delete') - async deleteExecutions(req: ExecutionRequest.Delete) { + async delete(req: ExecutionRequest.Delete) { const workflowIds = await this.getAccessibleWorkflowIds(req.user); - return await this.executionService.deleteExecutions(req, workflowIds); + if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); + + return await this.executionService.delete(req, workflowIds); } } diff --git a/packages/cli/test/unit/active-execution.service.test.ts b/packages/cli/test/unit/active-execution.service.test.ts new file mode 100644 index 0000000000000..60a8fa48cfcfb --- /dev/null +++ b/packages/cli/test/unit/active-execution.service.test.ts @@ -0,0 +1,127 @@ +import { mock, mockFn } from 'jest-mock-extended'; +import { ActiveExecutionService } from '@/executions/active-execution.service'; +import config from '@/config'; +import type { ExecutionRepository } from '@db/repositories/execution.repository'; +import type { ActiveExecutions } from '@/ActiveExecutions'; +import type { Job, Queue } from '@/Queue'; +import type { IExecutionBase, IExecutionsCurrentSummary } from '@/Interfaces'; +import type { WaitTracker } from '@/WaitTracker'; + +describe('ActiveExecutionsService', () => { + const queue = mock(); + const activeExecutions = mock(); + const executionRepository = mock(); + const waitTracker = mock(); + + const jobIds = ['j1', 'j2']; + const jobs = jobIds.map((executionId) => mock({ data: { executionId } })); + + const activeExecutionService = new ActiveExecutionService( + mock(), + queue, + activeExecutions, + executionRepository, + waitTracker, + ); + + const getEnv = mockFn<(typeof config)['getEnv']>(); + config.getEnv = getEnv; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('stop()', () => { + describe('in regular mode', () => { + getEnv.calledWith('executions.mode').mockReturnValue('regular'); + + it('should call `ActiveExecutions.stopExecution()`', async () => { + const execution = mock({ id: '123' }); + + await activeExecutionService.stop(execution); + + expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id); + }); + + it('should call `WaitTracker.stopExecution()` if `ActiveExecutions.stopExecution()` found no execution', async () => { + activeExecutions.stopExecution.mockResolvedValue(undefined); + const execution = mock({ id: '123' }); + + await activeExecutionService.stop(execution); + + expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id); + }); + }); + + describe('in queue mode', () => { + it('should call `ActiveExecutions.stopExecution()`', async () => { + const execution = mock({ id: '123' }); + + await activeExecutionService.stop(execution); + + expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id); + }); + + it('should call `WaitTracker.stopExecution` if `ActiveExecutions.stopExecution()` found no execution', async () => { + activeExecutions.stopExecution.mockResolvedValue(undefined); + const execution = mock({ id: '123' }); + + await activeExecutionService.stop(execution); + + expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id); + }); + }); + }); + + describe('findManyInQueueMode()', () => { + it('should query for active jobs, waiting jobs, and in-memory executions', async () => { + const sharedWorkflowIds = ['123']; + const filter = {}; + const executionIds = ['e1', 'e2']; + const summaries = executionIds.map((e) => mock({ id: e })); + + activeExecutions.getActiveExecutions.mockReturnValue(summaries); + queue.getJobs.mockResolvedValue(jobs); + executionRepository.findMultipleExecutions.mockResolvedValue([]); + executionRepository.getManyActive.mockResolvedValue([]); + + await activeExecutionService.findManyInQueueMode(filter, sharedWorkflowIds); + + expect(queue.getJobs).toHaveBeenCalledWith(['active', 'waiting']); + + expect(executionRepository.getManyActive).toHaveBeenCalledWith( + jobIds.concat(executionIds), + sharedWorkflowIds, + filter, + ); + }); + }); + + describe('findManyInRegularMode()', () => { + it('should return summaries of in-memory executions', async () => { + const sharedWorkflowIds = ['123']; + const filter = {}; + const executionIds = ['e1', 'e2']; + const summaries = executionIds.map((e) => + mock({ id: e, workflowId: '123', status: 'running' }), + ); + + activeExecutions.getActiveExecutions.mockReturnValue(summaries); + + const result = await activeExecutionService.findManyInRegularMode(filter, sharedWorkflowIds); + + expect(result).toEqual([ + expect.objectContaining({ + id: 'e1', + workflowId: '123', + status: 'running', + }), + expect.objectContaining({ + id: 'e2', + workflowId: '123', + status: 'running', + }), + ]); + }); + }); +}); diff --git a/packages/cli/test/unit/controllers/executions.controller.test.ts b/packages/cli/test/unit/controllers/executions.controller.test.ts new file mode 100644 index 0000000000000..ccae675cb95ac --- /dev/null +++ b/packages/cli/test/unit/controllers/executions.controller.test.ts @@ -0,0 +1,90 @@ +import { mock, mockFn } from 'jest-mock-extended'; +import config from '@/config'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { ExecutionsController } from '@/executions/executions.controller'; +import { License } from '@/License'; +import { mockInstance } from '../../shared/mocking'; +import type { IExecutionBase } from '@/Interfaces'; +import type { ActiveExecutionService } from '@/executions/active-execution.service'; +import type { ExecutionRequest } from '@/executions/execution.types'; +import type { WorkflowSharingService } from '@/workflows/workflowSharing.service'; + +describe('ExecutionsController', () => { + const getEnv = mockFn<(typeof config)['getEnv']>(); + config.getEnv = getEnv; + + mockInstance(License); + const activeExecutionService = mock(); + const workflowSharingService = mock(); + + const req = mock({ query: { filter: '{}' } }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getActive()', () => { + workflowSharingService.getSharedWorkflowIds.mockResolvedValue(['123']); + + it('should call `ActiveExecutionService.findManyInQueueMode()`', async () => { + getEnv.calledWith('executions.mode').mockReturnValue('queue'); + + await new ExecutionsController( + mock(), + mock(), + workflowSharingService, + activeExecutionService, + ).getActive(req); + + expect(activeExecutionService.findManyInQueueMode).toHaveBeenCalled(); + expect(activeExecutionService.findManyInRegularMode).not.toHaveBeenCalled(); + }); + + it('should call `ActiveExecutionService.findManyInRegularMode()`', async () => { + getEnv.calledWith('executions.mode').mockReturnValue('regular'); + + await new ExecutionsController( + mock(), + mock(), + workflowSharingService, + activeExecutionService, + ).getActive(req); + + expect(activeExecutionService.findManyInQueueMode).not.toHaveBeenCalled(); + expect(activeExecutionService.findManyInRegularMode).toHaveBeenCalled(); + }); + }); + + describe('stop()', () => { + const req = mock({ params: { id: '999' } }); + const execution = mock(); + + it('should 404 when execution is not found or inaccessible for user', async () => { + activeExecutionService.findOne.mockResolvedValue(undefined); + + const promise = new ExecutionsController( + mock(), + mock(), + workflowSharingService, + activeExecutionService, + ).stop(req); + + await expect(promise).rejects.toThrow(NotFoundError); + expect(activeExecutionService.findOne).toHaveBeenCalledWith('999', ['123']); + }); + + it('should call `ActiveExecutionService.stop()`', async () => { + getEnv.calledWith('executions.mode').mockReturnValue('regular'); + activeExecutionService.findOne.mockResolvedValue(execution); + + await new ExecutionsController( + mock(), + mock(), + workflowSharingService, + activeExecutionService, + ).stop(req); + + expect(activeExecutionService.stop).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts index 5f5f566162ede..82ae637c4247b 100644 --- a/packages/editor-ui/src/api/workflows.ts +++ b/packages/editor-ui/src/api/workflows.ts @@ -27,8 +27,8 @@ export async function getActiveWorkflows(context: IRestApiContext) { return await makeRestApiRequest(context, 'GET', '/active-workflows'); } -export async function getCurrentExecutions(context: IRestApiContext, filter: IDataObject) { - return await makeRestApiRequest(context, 'GET', '/executions-current', { filter }); +export async function getActiveExecutions(context: IRestApiContext, filter: IDataObject) { + return await makeRestApiRequest(context, 'GET', '/executions/active', { filter }); } export async function getExecutions( diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index 749e09834c111..b9e6cb633a8b4 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -553,7 +553,7 @@ export default defineComponent({ }, async loadActiveExecutions(): Promise { const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata) - ? await this.workflowsStore.getCurrentExecutions(this.workflowFilterCurrent) + ? await this.workflowsStore.getActiveExecutions(this.workflowFilterCurrent) : []; for (const activeExecution of activeExecutions) { if (activeExecution.workflowId && !activeExecution.workflowName) { @@ -573,7 +573,7 @@ export default defineComponent({ // ever get ids 500, 501, 502 and 503 when they finish const promises = [this.workflowsStore.getPastExecutions(filter, this.requestItemsPerRequest)]; if (isEmpty(filter.metadata)) { - promises.push(this.workflowsStore.getCurrentExecutions({})); + promises.push(this.workflowsStore.getActiveExecutions({})); } const results = await Promise.all(promises); diff --git a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts index ad20ade1942cb..65c6ce2e887f4 100644 --- a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts +++ b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts @@ -111,7 +111,7 @@ describe('ExecutionsList.vue', () => { workflowsStore = useWorkflowsStore(); vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData); - vi.spyOn(workflowsStore, 'getCurrentExecutions').mockResolvedValue([]); + vi.spyOn(workflowsStore, 'getActiveExecutions').mockResolvedValue([]); }); it('should render empty list', async () => { diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 719e7b176851d..cd833f9f0389e 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -64,7 +64,7 @@ import { findLast } from 'lodash-es'; import { useRootStore } from '@/stores/n8nRoot.store'; import { getActiveWorkflows, - getCurrentExecutions, + getActiveExecutions, getExecutionData, getExecutions, getNewWorkflow, @@ -1276,7 +1276,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData); }, - async getCurrentExecutions(filter: IDataObject): Promise { + async getActiveExecutions(filter: IDataObject): Promise { let sendData = {}; if (filter) { sendData = { @@ -1287,7 +1287,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return await makeRestApiRequest( rootStore.getRestApiContext, 'GET', - '/executions-current', + '/executions/active', sendData, ); }, @@ -1355,7 +1355,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return await makeRestApiRequest( rootStore.getRestApiContext, 'POST', - `/executions-current/${executionId}/stop`, + `/executions/active/${executionId}/stop`, ); }, @@ -1370,7 +1370,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { try { const rootStore = useRootStore(); if ((!requestFilter.status || !requestFilter.finished) && isEmpty(requestFilter.metadata)) { - activeExecutions = await getCurrentExecutions(rootStore.getRestApiContext, { + activeExecutions = await getActiveExecutions(rootStore.getRestApiContext, { workflowId: requestFilter.workflowId, }); } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index b3ca075803bf4..03f8bcd6cbe29 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2158,7 +2158,7 @@ export interface NodeExecutionWithMetadata extends INodeExecutionData { pairedItem: IPairedItemData | IPairedItemData[]; } -export interface IExecutionsSummary { +export interface ExecutionSummary { id: string; finished?: boolean; mode: WorkflowExecuteMode;