From 197ef5c27ba5ab76c0db25fa2f2f1ed04d588da2 Mon Sep 17 00:00:00 2001 From: Ivan Atanasov Date: Wed, 30 Oct 2024 11:20:34 +0100 Subject: [PATCH] add execution statistics to workflow list --- .../workflow-statistics.controller.ts | 55 ++----------------- packages/cli/src/interfaces.ts | 7 +++ .../services/workflow-statistics.service.ts | 41 +++++++++++++- .../cli/src/workflows/workflow.request.ts | 7 ++- .../cli/src/workflows/workflow.service.ts | 25 ++++++++- .../cli/src/workflows/workflows.controller.ts | 1 + packages/editor-ui/src/Interface.ts | 6 ++ 7 files changed, 89 insertions(+), 53 deletions(-) diff --git a/packages/cli/src/controllers/workflow-statistics.controller.ts b/packages/cli/src/controllers/workflow-statistics.controller.ts index 58c99727db..e18172919b 100644 --- a/packages/cli/src/controllers/workflow-statistics.controller.ts +++ b/packages/cli/src/controllers/workflow-statistics.controller.ts @@ -1,28 +1,23 @@ import { Response, NextFunction } from 'express'; -import type { WorkflowStatistics } from '@/databases/entities/workflow-statistics'; import { StatisticsNames } from '@/databases/entities/workflow-statistics'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository'; import { Get, Middleware, RestController } from '@/decorators'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import type { IWorkflowStatisticsDataLoaded } from '@/interfaces'; +import type { IWorkflowStatisticsDataLoaded, WorkflowStatisticsData } from '@/interfaces'; import { Logger } from '@/logging/logger.service'; -import { StatisticsRequest } from './workflow-statistics.types'; +import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; -interface WorkflowStatisticsData { - productionSuccess: T; - productionError: T; - manualSuccess: T; - manualError: T; -} +import { StatisticsRequest } from './workflow-statistics.types'; @RestController('/workflow-stats') export class WorkflowStatisticsController { constructor( private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly workflowStatisticsRepository: WorkflowStatisticsRepository, + private readonly workflowStatisticsService: WorkflowStatisticsService, private readonly logger: Logger, ) {} @@ -53,12 +48,12 @@ export class WorkflowStatisticsController { @Get('/:id/counts/') async getCounts(req: StatisticsRequest.GetOne): Promise> { - return await this.getData(req.params.id, 'count', 0); + return await this.workflowStatisticsService.getData(req.params.id, 'count', 0); } @Get('/:id/times/') async getTimes(req: StatisticsRequest.GetOne): Promise> { - return await this.getData(req.params.id, 'latestEvent', null); + return await this.workflowStatisticsService.getData(req.params.id, 'latestEvent', null); } @Get('/:id/data-loaded/') @@ -79,42 +74,4 @@ export class WorkflowStatisticsController { dataLoaded: stats ? true : false, }; } - - private async getData< - C extends 'count' | 'latestEvent', - D = WorkflowStatistics[C] extends number ? 0 : null, - >(workflowId: string, columnName: C, defaultValue: WorkflowStatistics[C] | D) { - const stats = await this.workflowStatisticsRepository.find({ - select: [columnName, 'name'], - where: { workflowId }, - }); - - const data: WorkflowStatisticsData = { - productionSuccess: defaultValue, - productionError: defaultValue, - manualSuccess: defaultValue, - manualError: defaultValue, - }; - - stats.forEach(({ name, [columnName]: value }) => { - switch (name) { - case StatisticsNames.manualError: - data.manualError = value; - break; - - case StatisticsNames.manualSuccess: - data.manualSuccess = value; - break; - - case StatisticsNames.productionError: - data.productionError = value; - break; - - case StatisticsNames.productionSuccess: - data.productionSuccess = value; - } - }); - - return data; - } } diff --git a/packages/cli/src/interfaces.ts b/packages/cli/src/interfaces.ts index 65b3a56346..7e70ab8d24 100644 --- a/packages/cli/src/interfaces.ts +++ b/packages/cli/src/interfaces.ts @@ -306,6 +306,13 @@ export interface IWorkflowStatisticsDataLoaded { dataLoaded: boolean; } +export interface WorkflowStatisticsData { + productionSuccess: T; + productionError: T; + manualSuccess: T; + manualError: T; +} + // ---------------------------------- // community nodes // ---------------------------------- diff --git a/packages/cli/src/services/workflow-statistics.service.ts b/packages/cli/src/services/workflow-statistics.service.ts index 53cbac5094..6c0c50a685 100644 --- a/packages/cli/src/services/workflow-statistics.service.ts +++ b/packages/cli/src/services/workflow-statistics.service.ts @@ -1,7 +1,7 @@ import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; import { Service } from 'typedi'; -import { StatisticsNames } from '@/databases/entities/workflow-statistics'; +import { StatisticsNames, WorkflowStatistics } from '@/databases/entities/workflow-statistics'; import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository'; import { EventService } from '@/events/event.service'; import { Logger } from '@/logging/logger.service'; @@ -9,6 +9,7 @@ import { UserService } from '@/services/user.service'; import { TypedEmitter } from '@/typed-emitter'; import { OwnershipService } from './ownership.service'; +import { WorkflowStatisticsData } from '@/interfaces'; type WorkflowStatisticsEvents = { nodeFetchedData: { workflowId: string; node: INode }; @@ -129,4 +130,42 @@ export class WorkflowStatisticsService extends TypedEmitter(workflowId: string, columnName: C, defaultValue: WorkflowStatistics[C] | D) { + const stats = await this.repository.find({ + select: [columnName, 'name'], + where: { workflowId }, + }); + + const data: WorkflowStatisticsData = { + productionSuccess: defaultValue, + productionError: defaultValue, + manualSuccess: defaultValue, + manualError: defaultValue, + }; + + stats.forEach(({ name, [columnName]: value }) => { + switch (name) { + case StatisticsNames.manualError: + data.manualError = value; + break; + + case StatisticsNames.manualSuccess: + data.manualSuccess = value; + break; + + case StatisticsNames.productionError: + data.productionError = value; + break; + + case StatisticsNames.productionSuccess: + data.productionSuccess = value; + } + }); + + return data; + } } diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index d45cfd14d3..83f0cb3e86 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -28,7 +28,12 @@ export declare namespace WorkflowRequest { type Get = AuthenticatedRequest<{ workflowId: string }>; - type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & { + type GetMany = AuthenticatedRequest< + {}, + {}, + {}, + ListQuery.Params & { includeScopes?: string; includeExecutionStatistics?: string } + > & { listQueryOptions: ListQuery.Options; }; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index bce8770303..a430cf7298 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -31,6 +31,7 @@ import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service'; import { RoleService } from '@/services/role.service'; import { TagService } from '@/services/tag.service'; +import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; @@ -55,9 +56,15 @@ export class WorkflowService { private readonly projectService: ProjectService, private readonly executionRepository: ExecutionRepository, private readonly eventService: EventService, + private readonly workflowStatisticsService: WorkflowStatisticsService, ) {} - async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) { + async getMany( + user: User, + options?: ListQuery.Options, + includeScopes?: boolean, + includeExecutionStatistics?: boolean, + ) { const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: ['workflow:read'], }); @@ -74,8 +81,22 @@ export class WorkflowService { workflows = workflows.map((w) => this.roleService.addScopes(w, user, projectRelations)); } + if (includeExecutionStatistics) { + workflows = await Promise.all( + workflows.map(async (w) => { + const stats = await this.workflowStatisticsService.getData(w.id, 'count', 0); + return { + ...w, + executionStatistics: { + errors: stats.productionError, + successes: stats.productionSuccess, + }, + }; + }), + ); + } + workflows.forEach((w) => { - // @ts-expect-error: This is to emulate the old behaviour of removing the shared // field as part of `addOwnedByAndSharedWith`. We need this field in `addScopes` // though. So to avoid leaking the information we just delete it. delete w.shared; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 59f53e0df1..1447cb80f0 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -198,6 +198,7 @@ export class WorkflowsController { req.user, req.listQueryOptions, !!req.query.includeScopes, + !!req.query.includeExecutionStatistics, ); res.json({ count, data }); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e639f537df..377b8fc6af 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -276,6 +276,11 @@ export interface WorkflowMetadata { templateCredsSetupCompleted?: boolean; } +export interface ExecutionStatistics { + errors: number; + successes: number; +} + // Almost identical to cli.Interfaces.ts export interface IWorkflowDb { id: string; @@ -294,6 +299,7 @@ export interface IWorkflowDb { versionId: string; usedCredentials?: IUsedCredential[]; meta?: WorkflowMetadata; + executionStatistics?: ExecutionStatistics; } // Identical to cli.Interfaces.ts