mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
add execution statistics to workflow list
This commit is contained in:
parent
497d637fc5
commit
197ef5c27b
|
@ -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<T> {
|
||||
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<WorkflowStatisticsData<number>> {
|
||||
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<WorkflowStatisticsData<Date | null>> {
|
||||
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<WorkflowStatistics[C] | D> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -306,6 +306,13 @@ export interface IWorkflowStatisticsDataLoaded {
|
|||
dataLoaded: boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowStatisticsData<T> {
|
||||
productionSuccess: T;
|
||||
productionError: T;
|
||||
manualSuccess: T;
|
||||
manualError: T;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// community nodes
|
||||
// ----------------------------------
|
||||
|
|
|
@ -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<WorkflowStatisticsEv
|
|||
|
||||
this.eventService.emit('first-workflow-data-loaded', metrics);
|
||||
}
|
||||
|
||||
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.repository.find({
|
||||
select: [columnName, 'name'],
|
||||
where: { workflowId },
|
||||
});
|
||||
|
||||
const data: WorkflowStatisticsData<WorkflowStatistics[C] | D> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -198,6 +198,7 @@ export class WorkflowsController {
|
|||
req.user,
|
||||
req.listQueryOptions,
|
||||
!!req.query.includeScopes,
|
||||
!!req.query.includeExecutionStatistics,
|
||||
);
|
||||
|
||||
res.json({ count, data });
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue