mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-25 11:31:38 -08:00
refactor(core): Separate execution startedAt
from createdAt
(#10810)
This commit is contained in:
parent
bf7392a878
commit
afda049491
|
@ -13,7 +13,7 @@ import { Service } from 'typedi';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
||||||
import type {
|
import type {
|
||||||
ExecutionPayload,
|
CreateExecutionPayload,
|
||||||
IExecutingWorkflowData,
|
IExecutingWorkflowData,
|
||||||
IExecutionDb,
|
IExecutionDb,
|
||||||
IExecutionsCurrentSummary,
|
IExecutionsCurrentSummary,
|
||||||
|
@ -52,11 +52,10 @@ export class ActiveExecutions {
|
||||||
if (executionId === undefined) {
|
if (executionId === undefined) {
|
||||||
// Is a new execution so save in DB
|
// Is a new execution so save in DB
|
||||||
|
|
||||||
const fullExecutionData: ExecutionPayload = {
|
const fullExecutionData: CreateExecutionPayload = {
|
||||||
data: executionData.executionData!,
|
data: executionData.executionData!,
|
||||||
mode,
|
mode,
|
||||||
finished: false,
|
finished: false,
|
||||||
startedAt: new Date(),
|
|
||||||
workflowData: executionData.workflowData,
|
workflowData: executionData.workflowData,
|
||||||
status: executionStatus,
|
status: executionStatus,
|
||||||
workflowId: executionData.workflowData.id,
|
workflowId: executionData.workflowData.id,
|
||||||
|
@ -74,7 +73,10 @@ export class ActiveExecutions {
|
||||||
executionId = await this.executionRepository.createNewExecution(fullExecutionData);
|
executionId = await this.executionRepository.createNewExecution(fullExecutionData);
|
||||||
assert(executionId);
|
assert(executionId);
|
||||||
|
|
||||||
await this.concurrencyControl.throttle({ mode, executionId });
|
if (config.getEnv('executions.mode') === 'regular') {
|
||||||
|
await this.concurrencyControl.throttle({ mode, executionId });
|
||||||
|
await this.executionRepository.setRunning(executionId);
|
||||||
|
}
|
||||||
executionStatus = 'running';
|
executionStatus = 'running';
|
||||||
} else {
|
} else {
|
||||||
// Is an existing execution we want to finish so update in DB
|
// Is an existing execution we want to finish so update in DB
|
||||||
|
@ -86,6 +88,7 @@ export class ActiveExecutions {
|
||||||
data: executionData.executionData!,
|
data: executionData.executionData!,
|
||||||
waitTill: null,
|
waitTill: null,
|
||||||
status: executionStatus,
|
status: executionStatus,
|
||||||
|
// this is resuming, so keep `startedAt` as it was
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.executionRepository.updateExistingExecution(executionId, execution);
|
await this.executionRepository.updateExistingExecution(executionId, execution);
|
||||||
|
|
|
@ -70,7 +70,6 @@ export class ConcurrencyControlService {
|
||||||
|
|
||||||
this.productionQueue.on('execution-released', async (executionId) => {
|
this.productionQueue.on('execution-released', async (executionId) => {
|
||||||
this.log('Execution released', { executionId });
|
this.log('Execution released', { executionId });
|
||||||
await this.executionRepository.resetStartedAt(executionId);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,14 @@ export class ExecutionEntity {
|
||||||
status: ExecutionStatus;
|
status: ExecutionStatus;
|
||||||
|
|
||||||
@Column(datetimeColumnType)
|
@Column(datetimeColumnType)
|
||||||
startedAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time when the processing of the execution actually started. This column
|
||||||
|
* is `null` when an execution is enqueued but has not started yet.
|
||||||
|
*/
|
||||||
|
@Column({ type: datetimeColumnType, nullable: true })
|
||||||
|
startedAt: Date | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ type: datetimeColumnType, nullable: true })
|
@Column({ type: datetimeColumnType, nullable: true })
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { MigrationContext, ReversibleMigration } from '@/databases/types';
|
||||||
|
|
||||||
|
export class SeparateExecutionCreationFromStart1727427440136 implements ReversibleMigration {
|
||||||
|
async up({
|
||||||
|
schemaBuilder: { addColumns, column, dropNotNull },
|
||||||
|
runQuery,
|
||||||
|
escape,
|
||||||
|
}: MigrationContext) {
|
||||||
|
await addColumns('execution_entity', [
|
||||||
|
column('createdAt').notNull.timestamp().default('NOW()'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await dropNotNull('execution_entity', 'startedAt');
|
||||||
|
|
||||||
|
const executionEntity = escape.tableName('execution_entity');
|
||||||
|
const createdAt = escape.columnName('createdAt');
|
||||||
|
const startedAt = escape.columnName('startedAt');
|
||||||
|
|
||||||
|
// inaccurate for pre-migration rows but prevents `createdAt` from being nullable
|
||||||
|
await runQuery(`UPDATE ${executionEntity} SET ${createdAt} = ${startedAt};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down({ schemaBuilder: { dropColumns, addNotNull } }: MigrationContext) {
|
||||||
|
await dropColumns('execution_entity', ['createdAt']);
|
||||||
|
await addNotNull('execution_entity', 'startedAt');
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ import { CreateInvalidAuthTokenTable1723627610222 } from '../common/172362761022
|
||||||
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
||||||
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
||||||
import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable';
|
import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable';
|
||||||
|
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
|
||||||
|
|
||||||
export const mysqlMigrations: Migration[] = [
|
export const mysqlMigrations: Migration[] = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -130,4 +131,5 @@ export const mysqlMigrations: Migration[] = [
|
||||||
RefactorExecutionIndices1723796243146,
|
RefactorExecutionIndices1723796243146,
|
||||||
CreateAnnotationTables1724753530828,
|
CreateAnnotationTables1724753530828,
|
||||||
AddApiKeysTable1724951148974,
|
AddApiKeysTable1724951148974,
|
||||||
|
SeparateExecutionCreationFromStart1727427440136,
|
||||||
];
|
];
|
||||||
|
|
|
@ -64,6 +64,7 @@ import { CreateInvalidAuthTokenTable1723627610222 } from '../common/172362761022
|
||||||
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
||||||
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
||||||
import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable';
|
import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable';
|
||||||
|
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
|
||||||
|
|
||||||
export const postgresMigrations: Migration[] = [
|
export const postgresMigrations: Migration[] = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -130,4 +131,5 @@ export const postgresMigrations: Migration[] = [
|
||||||
RefactorExecutionIndices1723796243146,
|
RefactorExecutionIndices1723796243146,
|
||||||
CreateAnnotationTables1724753530828,
|
CreateAnnotationTables1724753530828,
|
||||||
AddApiKeysTable1724951148974,
|
AddApiKeysTable1724951148974,
|
||||||
|
SeparateExecutionCreationFromStart1727427440136,
|
||||||
];
|
];
|
||||||
|
|
|
@ -61,6 +61,7 @@ import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101
|
||||||
import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable';
|
import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable';
|
||||||
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
|
||||||
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';
|
||||||
|
import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart';
|
||||||
|
|
||||||
const sqliteMigrations: Migration[] = [
|
const sqliteMigrations: Migration[] = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -124,6 +125,7 @@ const sqliteMigrations: Migration[] = [
|
||||||
RefactorExecutionIndices1723796243146,
|
RefactorExecutionIndices1723796243146,
|
||||||
CreateAnnotationTables1724753530828,
|
CreateAnnotationTables1724753530828,
|
||||||
AddApiKeysTable1724951148974,
|
AddApiKeysTable1724951148974,
|
||||||
|
SeparateExecutionCreationFromStart1727427440136,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
|
@ -42,7 +42,7 @@ import { ExecutionAnnotation } from '@/databases/entities/execution-annotation.e
|
||||||
import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error';
|
import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error';
|
||||||
import type { ExecutionSummaries } from '@/executions/execution.types';
|
import type { ExecutionSummaries } from '@/executions/execution.types';
|
||||||
import type {
|
import type {
|
||||||
ExecutionPayload,
|
CreateExecutionPayload,
|
||||||
IExecutionBase,
|
IExecutionBase,
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
|
@ -198,7 +198,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
return executions.map((execution) => {
|
return executions.map((execution) => {
|
||||||
const { executionData, ...rest } = execution;
|
const { executionData, ...rest } = execution;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
}) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[];
|
||||||
}
|
}
|
||||||
|
|
||||||
reportInvalidExecutions(executions: ExecutionEntity[]) {
|
reportInvalidExecutions(executions: ExecutionEntity[]) {
|
||||||
|
@ -297,15 +297,15 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
}),
|
}),
|
||||||
...(options?.includeAnnotation &&
|
...(options?.includeAnnotation &&
|
||||||
serializedAnnotation && { annotation: serializedAnnotation }),
|
serializedAnnotation && { annotation: serializedAnnotation }),
|
||||||
};
|
} as IExecutionFlattedDb | IExecutionResponse | IExecutionBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a new execution and its execution data using a transaction.
|
* Insert a new execution and its execution data using a transaction.
|
||||||
*/
|
*/
|
||||||
async createNewExecution(execution: ExecutionPayload): Promise<string> {
|
async createNewExecution(execution: CreateExecutionPayload): Promise<string> {
|
||||||
const { data, workflowData, ...rest } = execution;
|
const { data, workflowData, ...rest } = execution;
|
||||||
const { identifiers: inserted } = await this.insert(rest);
|
const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() });
|
||||||
const { id: executionId } = inserted[0] as { id: string };
|
const { id: executionId } = inserted[0] as { id: string };
|
||||||
const { connections, nodes, name, settings } = workflowData ?? {};
|
const { connections, nodes, name, settings } = workflowData ?? {};
|
||||||
await this.executionDataRepository.insert({
|
await this.executionDataRepository.insert({
|
||||||
|
@ -344,16 +344,25 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
await this.update({ id: executionId }, { status });
|
await this.update({ id: executionId }, { status });
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetStartedAt(executionId: string) {
|
async setRunning(executionId: string) {
|
||||||
await this.update({ id: executionId }, { startedAt: new Date() });
|
const startedAt = new Date();
|
||||||
|
|
||||||
|
await this.update({ id: executionId }, { status: 'running', startedAt });
|
||||||
|
|
||||||
|
return startedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExistingExecution(executionId: string, execution: Partial<IExecutionResponse>) {
|
async updateExistingExecution(executionId: string, execution: Partial<IExecutionResponse>) {
|
||||||
// Se isolate startedAt because it must be set when the execution starts and should never change.
|
const {
|
||||||
// So we prevent updating it, if it's sent (it usually is and causes problems to executions that
|
id,
|
||||||
// are resumed after waiting for some time, as a new startedAt is set)
|
data,
|
||||||
const { id, data, workflowId, workflowData, startedAt, customData, ...executionInformation } =
|
workflowId,
|
||||||
execution;
|
workflowData,
|
||||||
|
createdAt, // must never change
|
||||||
|
startedAt, // must never change
|
||||||
|
customData,
|
||||||
|
...executionInformation
|
||||||
|
} = execution;
|
||||||
if (Object.keys(executionInformation).length > 0) {
|
if (Object.keys(executionInformation).length > 0) {
|
||||||
await this.update({ id: executionId }, executionInformation);
|
await this.update({ id: executionId }, executionInformation);
|
||||||
}
|
}
|
||||||
|
@ -721,6 +730,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
mode: true,
|
mode: true,
|
||||||
retryOf: true,
|
retryOf: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
startedAt: true,
|
startedAt: true,
|
||||||
stoppedAt: true,
|
stoppedAt: true,
|
||||||
};
|
};
|
||||||
|
@ -806,6 +816,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
// @tech_debt: These transformations should not be needed
|
// @tech_debt: These transformations should not be needed
|
||||||
private toSummary(execution: {
|
private toSummary(execution: {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
|
createdAt?: Date | string;
|
||||||
startedAt?: Date | string;
|
startedAt?: Date | string;
|
||||||
stoppedAt?: Date | string;
|
stoppedAt?: Date | string;
|
||||||
waitTill?: Date | string | null;
|
waitTill?: Date | string | null;
|
||||||
|
@ -817,6 +828,13 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
return date;
|
return date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (execution.createdAt) {
|
||||||
|
execution.createdAt =
|
||||||
|
execution.createdAt instanceof Date
|
||||||
|
? execution.createdAt.toISOString()
|
||||||
|
: normalizeDateString(execution.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
if (execution.startedAt) {
|
if (execution.startedAt) {
|
||||||
execution.startedAt =
|
execution.startedAt =
|
||||||
execution.startedAt instanceof Date
|
execution.startedAt instanceof Date
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { ExecutionStatus, IRun, IWorkflowBase } from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import type { ExecutionPayload, IExecutionDb } from '@/interfaces';
|
import type { IExecutionDb, UpdateExecutionPayload } from '@/interfaces';
|
||||||
import { Logger } from '@/logger';
|
import { Logger } from '@/logger';
|
||||||
import { ExecutionMetadataService } from '@/services/execution-metadata.service';
|
import { ExecutionMetadataService } from '@/services/execution-metadata.service';
|
||||||
import { isWorkflowIdValid } from '@/utils';
|
import { isWorkflowIdValid } from '@/utils';
|
||||||
|
@ -46,7 +46,7 @@ export function prepareExecutionDataForDbUpdate(parameters: {
|
||||||
'pinData',
|
'pinData',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fullExecutionData: ExecutionPayload = {
|
const fullExecutionData: UpdateExecutionPayload = {
|
||||||
data: runData.data,
|
data: runData.data,
|
||||||
mode: runData.mode,
|
mode: runData.mode,
|
||||||
finished: runData.finished ? runData.finished : false,
|
finished: runData.finished ? runData.finished : false,
|
||||||
|
|
|
@ -32,7 +32,7 @@ import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import type {
|
import type {
|
||||||
ExecutionPayload,
|
CreateExecutionPayload,
|
||||||
IExecutionFlattedResponse,
|
IExecutionFlattedResponse,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
|
@ -321,11 +321,10 @@ export class ExecutionService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fullExecutionData: ExecutionPayload = {
|
const fullExecutionData: CreateExecutionPayload = {
|
||||||
data: executionData,
|
data: executionData,
|
||||||
mode,
|
mode,
|
||||||
finished: false,
|
finished: false,
|
||||||
startedAt: new Date(),
|
|
||||||
workflowData,
|
workflowData,
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
stoppedAt: new Date(),
|
stoppedAt: new Date(),
|
||||||
|
|
|
@ -115,6 +115,7 @@ export type SaveExecutionDataType = 'all' | 'none';
|
||||||
export interface IExecutionBase {
|
export interface IExecutionBase {
|
||||||
id: string;
|
id: string;
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
|
createdAt: Date; // set by DB
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
stoppedAt?: Date; // empty value means execution is still running
|
stoppedAt?: Date; // empty value means execution is still running
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
@ -131,10 +132,11 @@ export interface IExecutionDb extends IExecutionBase {
|
||||||
workflowData: IWorkflowBase;
|
workflowData: IWorkflowBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Payload for creating an execution. */
|
||||||
* Payload for creating or updating an execution.
|
export type CreateExecutionPayload = Omit<IExecutionDb, 'id' | 'createdAt' | 'startedAt'>;
|
||||||
*/
|
|
||||||
export type ExecutionPayload = Omit<IExecutionDb, 'id'>;
|
/** Payload for updating an execution. */
|
||||||
|
export type UpdateExecutionPayload = Omit<IExecutionDb, 'id' | 'createdAt'>;
|
||||||
|
|
||||||
export interface IExecutionResponse extends IExecutionBase {
|
export interface IExecutionResponse extends IExecutionBase {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class JobProcessor {
|
||||||
|
|
||||||
this.logger.info(`[JobProcessor] Starting job ${job.id} (execution ${executionId})`);
|
this.logger.info(`[JobProcessor] Starting job ${job.id} (execution ${executionId})`);
|
||||||
|
|
||||||
await this.executionRepository.updateStatus(executionId, 'running');
|
const startedAt = await this.executionRepository.setRunning(executionId);
|
||||||
|
|
||||||
let { staticData } = execution.workflowData;
|
let { staticData } = execution.workflowData;
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ export class JobProcessor {
|
||||||
workflowId: execution.workflowId,
|
workflowId: execution.workflowId,
|
||||||
workflowName: execution.workflowData.name,
|
workflowName: execution.workflowData.name,
|
||||||
mode: execution.mode,
|
mode: execution.mode,
|
||||||
startedAt: execution.startedAt,
|
startedAt,
|
||||||
retryOf: execution.retryOf ?? '',
|
retryOf: execution.retryOf ?? '',
|
||||||
status: execution.status,
|
status: execution.status,
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,7 +41,11 @@ import config from '@/config';
|
||||||
import { CredentialsHelper } from '@/credentials-helper';
|
import { CredentialsHelper } from '@/credentials-helper';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { ExternalHooks } from '@/external-hooks';
|
import { ExternalHooks } from '@/external-hooks';
|
||||||
import type { IWorkflowExecuteProcess, IWorkflowErrorData, ExecutionPayload } from '@/interfaces';
|
import type {
|
||||||
|
IWorkflowExecuteProcess,
|
||||||
|
IWorkflowErrorData,
|
||||||
|
UpdateExecutionPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
|
@ -865,7 +869,7 @@ async function executeWorkflow(
|
||||||
// Therefore, database might not contain finished errors.
|
// Therefore, database might not contain finished errors.
|
||||||
// Force an update to db as there should be no harm doing this
|
// Force an update to db as there should be no harm doing this
|
||||||
|
|
||||||
const fullExecutionData: ExecutionPayload = {
|
const fullExecutionData: UpdateExecutionPayload = {
|
||||||
data: fullRunData.data,
|
data: fullRunData.data,
|
||||||
mode: fullRunData.mode,
|
mode: fullRunData.mode,
|
||||||
finished: fullRunData.finished ? fullRunData.finished : false,
|
finished: fullRunData.finished ? fullRunData.finished : false,
|
||||||
|
|
|
@ -22,7 +22,7 @@ import type { Project } from '@/databases/entities/project';
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import type { ExecutionPayload, IWorkflowDb, IWorkflowErrorData } from '@/interfaces';
|
import type { CreateExecutionPayload, IWorkflowDb, IWorkflowErrorData } from '@/interfaces';
|
||||||
import { Logger } from '@/logger';
|
import { Logger } from '@/logger';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service';
|
import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service';
|
||||||
|
@ -206,11 +206,10 @@ export class WorkflowExecutionService {
|
||||||
initialNode,
|
initialNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
const fullExecutionData: ExecutionPayload = {
|
const fullExecutionData: CreateExecutionPayload = {
|
||||||
data: fakeExecution.data,
|
data: fakeExecution.data,
|
||||||
mode: fakeExecution.mode,
|
mode: fakeExecution.mode,
|
||||||
finished: false,
|
finished: false,
|
||||||
startedAt: new Date(),
|
|
||||||
stoppedAt: new Date(),
|
stoppedAt: new Date(),
|
||||||
workflowData,
|
workflowData,
|
||||||
waitTill: null,
|
waitTill: null,
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe('ExecutionService', () => {
|
||||||
mode: expect.any(String),
|
mode: expect.any(String),
|
||||||
retryOf: null,
|
retryOf: null,
|
||||||
status: expect.any(String),
|
status: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
startedAt: expect.any(String),
|
startedAt: expect.any(String),
|
||||||
stoppedAt: expect.any(String),
|
stoppedAt: expect.any(String),
|
||||||
waitTill: null,
|
waitTill: null,
|
||||||
|
@ -510,6 +511,7 @@ describe('ExecutionService', () => {
|
||||||
mode: expect.any(String),
|
mode: expect.any(String),
|
||||||
retryOf: null,
|
retryOf: null,
|
||||||
status: expect.any(String),
|
status: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
startedAt: expect.any(String),
|
startedAt: expect.any(String),
|
||||||
stoppedAt: expect.any(String),
|
stoppedAt: expect.any(String),
|
||||||
waitTill: null,
|
waitTill: null,
|
||||||
|
|
|
@ -159,6 +159,7 @@ test('should report credential in not recently executed workflow', async () => {
|
||||||
const savedExecution = await Container.get(ExecutionRepository).save({
|
const savedExecution = await Container.get(ExecutionRepository).save({
|
||||||
finished: true,
|
finished: true,
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
|
createdAt: date,
|
||||||
startedAt: date,
|
startedAt: date,
|
||||||
stoppedAt: date,
|
stoppedAt: date,
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
|
@ -227,6 +228,7 @@ test('should not report credentials in recently executed workflow', async () =>
|
||||||
const savedExecution = await Container.get(ExecutionRepository).save({
|
const savedExecution = await Container.get(ExecutionRepository).save({
|
||||||
finished: true,
|
finished: true,
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
|
createdAt: date,
|
||||||
startedAt: date,
|
startedAt: date,
|
||||||
stoppedAt: date,
|
stoppedAt: date,
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
|
|
|
@ -39,6 +39,7 @@ export async function createExecution(
|
||||||
const execution = await Container.get(ExecutionRepository).save({
|
const execution = await Container.get(ExecutionRepository).save({
|
||||||
finished: finished ?? true,
|
finished: finished ?? true,
|
||||||
mode: mode ?? 'manual',
|
mode: mode ?? 'manual',
|
||||||
|
createdAt: new Date(),
|
||||||
startedAt: startedAt ?? new Date(),
|
startedAt: startedAt ?? new Date(),
|
||||||
...(workflow !== undefined && { workflowId: workflow.id }),
|
...(workflow !== undefined && { workflowId: workflow.id }),
|
||||||
stoppedAt: stoppedAt ?? new Date(),
|
stoppedAt: stoppedAt ?? new Date(),
|
||||||
|
|
|
@ -212,6 +212,7 @@
|
||||||
--execution-selector-background: var(--prim-gray-740);
|
--execution-selector-background: var(--prim-gray-740);
|
||||||
--execution-selector-text: var(--color-text-base);
|
--execution-selector-text: var(--color-text-base);
|
||||||
--execution-select-all-text: var(--color-text-base);
|
--execution-select-all-text: var(--color-text-base);
|
||||||
|
--execution-card-text-waiting: var(--prim-color-secondary-tint-100);
|
||||||
|
|
||||||
// NDV
|
// NDV
|
||||||
--color-run-data-background: var(--prim-gray-800);
|
--color-run-data-background: var(--prim-gray-800);
|
||||||
|
|
|
@ -273,6 +273,7 @@
|
||||||
--execution-card-border-running: var(--prim-color-alt-b-tint-250);
|
--execution-card-border-running: var(--prim-color-alt-b-tint-250);
|
||||||
--execution-card-border-unknown: var(--prim-gray-120);
|
--execution-card-border-unknown: var(--prim-gray-120);
|
||||||
--execution-card-background-hover: var(--color-foreground-light);
|
--execution-card-background-hover: var(--color-foreground-light);
|
||||||
|
--execution-card-text-waiting: var(--color-secondary);
|
||||||
--execution-selector-background: var(--color-background-dark);
|
--execution-selector-background: var(--color-background-dark);
|
||||||
--execution-selector-text: var(--color-text-xlight);
|
--execution-selector-text: var(--color-text-xlight);
|
||||||
--execution-select-all-text: var(--color-danger);
|
--execution-select-all-text: var(--color-danger);
|
||||||
|
|
|
@ -62,7 +62,9 @@ const classes = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedStartedAtDate = computed(() => {
|
const formattedStartedAtDate = computed(() => {
|
||||||
return props.execution.startedAt ? formatDate(props.execution.startedAt) : '';
|
return props.execution.startedAt
|
||||||
|
? formatDate(props.execution.startedAt)
|
||||||
|
: i18n.baseText('executionsList.startingSoon');
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedWaitTillDate = computed(() => {
|
const formattedWaitTillDate = computed(() => {
|
||||||
|
|
|
@ -150,4 +150,25 @@ describe('WorkflowExecutionsCard', () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('displays correct text for new execution', () => {
|
||||||
|
const createdAt = new Date('2024-09-27T12:00:00Z');
|
||||||
|
const props = {
|
||||||
|
execution: {
|
||||||
|
id: '1',
|
||||||
|
mode: 'manual',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: createdAt.toISOString(),
|
||||||
|
},
|
||||||
|
workflowPermissions: {
|
||||||
|
execute: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({ props });
|
||||||
|
|
||||||
|
const executionTimeElement = getByTestId('execution-time');
|
||||||
|
expect(executionTimeElement).toBeVisible();
|
||||||
|
expect(executionTimeElement.textContent).toBe('27 Sep - Starting soon');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||||
import type { PermissionsRecord } from '@/permissions';
|
import type { PermissionsRecord } from '@/permissions';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
execution: ExecutionSummary;
|
execution: ExecutionSummary;
|
||||||
|
@ -87,7 +88,17 @@ function onRetryMenuItemSelect(action: string): void {
|
||||||
:data-test-execution-status="executionUIDetails.name"
|
:data-test-execution-status="executionUIDetails.name"
|
||||||
>
|
>
|
||||||
<div :class="$style.description">
|
<div :class="$style.description">
|
||||||
<N8nText color="text-dark" :bold="true" size="medium" data-test-id="execution-time">
|
<N8nText
|
||||||
|
v-if="executionUIDetails.name === 'new'"
|
||||||
|
color="text-dark"
|
||||||
|
:bold="true"
|
||||||
|
size="medium"
|
||||||
|
data-test-id="execution-time"
|
||||||
|
>
|
||||||
|
{{ toDayMonth(executionUIDetails.createdAt) }} -
|
||||||
|
{{ locale.baseText('executionDetails.startingSoon') }}
|
||||||
|
</N8nText>
|
||||||
|
<N8nText v-else color="text-dark" :bold="true" size="medium" data-test-id="execution-time">
|
||||||
{{ executionUIDetails.startTime }}
|
{{ executionUIDetails.startTime }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<div :class="$style.executionStatus">
|
<div :class="$style.executionStatus">
|
||||||
|
@ -106,6 +117,15 @@ function onRetryMenuItemSelect(action: string): void {
|
||||||
{{ locale.baseText('executionDetails.runningTimeRunning') }}
|
{{ locale.baseText('executionDetails.runningTimeRunning') }}
|
||||||
<ExecutionsTime :start-time="execution.startedAt" />
|
<ExecutionsTime :start-time="execution.startedAt" />
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
<N8nText
|
||||||
|
v-if="executionUIDetails.name === 'new' && execution.createdAt"
|
||||||
|
:color="isActive ? 'text-dark' : 'text-base'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
>{{ locale.baseText('executionDetails.at') }} {{ toTime(execution.createdAt) }}</span
|
||||||
|
>
|
||||||
|
</N8nText>
|
||||||
<N8nText
|
<N8nText
|
||||||
v-else-if="executionUIDetails.runningTime !== ''"
|
v-else-if="executionUIDetails.runningTime !== ''"
|
||||||
:color="isActive ? 'text-dark' : 'text-base'"
|
:color="isActive ? 'text-dark' : 'text-base'"
|
||||||
|
@ -216,10 +236,10 @@ function onRetryMenuItemSelect(action: string): void {
|
||||||
&.new {
|
&.new {
|
||||||
&,
|
&,
|
||||||
& .executionLink {
|
& .executionLink {
|
||||||
border-left: var(--spacing-4xs) var(--border-style-base) var(--execution-card-border-new);
|
border-left: var(--spacing-4xs) var(--border-style-base) var(--execution-card-border-waiting);
|
||||||
}
|
}
|
||||||
.statusLabel {
|
.statusLabel {
|
||||||
color: var(--color-text-dark);
|
color: var(--execution-card-text-waiting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||||
export interface IExecutionUIData {
|
export interface IExecutionUIData {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
createdAt: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
runningTime: string;
|
runningTime: string;
|
||||||
showTimestamp: boolean;
|
showTimestamp: boolean;
|
||||||
|
@ -17,6 +18,7 @@ export function useExecutionHelpers() {
|
||||||
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
||||||
const status = {
|
const status = {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
|
createdAt: execution.createdAt?.toString() ?? '',
|
||||||
startTime: formatDate(execution.startedAt),
|
startTime: formatDate(execution.startedAt),
|
||||||
label: 'Status unknown',
|
label: 'Status unknown',
|
||||||
runningTime: '',
|
runningTime: '',
|
||||||
|
|
|
@ -658,6 +658,7 @@
|
||||||
"executionDetails.executionWaiting": "Execution waiting",
|
"executionDetails.executionWaiting": "Execution waiting",
|
||||||
"executionDetails.executionWasSuccessful": "Execution was successful",
|
"executionDetails.executionWasSuccessful": "Execution was successful",
|
||||||
"executionDetails.of": "of",
|
"executionDetails.of": "of",
|
||||||
|
"executionDetails.at": "at",
|
||||||
"executionDetails.newMessage": "Execution waiting in the queue.",
|
"executionDetails.newMessage": "Execution waiting in the queue.",
|
||||||
"executionDetails.openWorkflow": "Open Workflow",
|
"executionDetails.openWorkflow": "Open Workflow",
|
||||||
"executionDetails.readOnly.readOnly": "Read only",
|
"executionDetails.readOnly.readOnly": "Read only",
|
||||||
|
@ -666,6 +667,7 @@
|
||||||
"executionDetails.runningTimeFinished": "in {time}",
|
"executionDetails.runningTimeFinished": "in {time}",
|
||||||
"executionDetails.runningTimeRunning": "for",
|
"executionDetails.runningTimeRunning": "for",
|
||||||
"executionDetails.runningMessage": "Execution is running. It will show here once finished.",
|
"executionDetails.runningMessage": "Execution is running. It will show here once finished.",
|
||||||
|
"executionDetails.startingSoon": "Starting soon",
|
||||||
"executionDetails.workflow": "workflow",
|
"executionDetails.workflow": "workflow",
|
||||||
"executionsLandingPage.emptyState.noTrigger.heading": "Set up the first step. Then execute your workflow",
|
"executionsLandingPage.emptyState.noTrigger.heading": "Set up the first step. Then execute your workflow",
|
||||||
"executionsLandingPage.emptyState.noTrigger.buttonText": "Add first step...",
|
"executionsLandingPage.emptyState.noTrigger.buttonText": "Add first step...",
|
||||||
|
@ -730,6 +732,7 @@
|
||||||
"executionsList.showMessage.stopExecution.message": "Execution ID {activeExecutionId}",
|
"executionsList.showMessage.stopExecution.message": "Execution ID {activeExecutionId}",
|
||||||
"executionsList.showMessage.stopExecution.title": "Execution stopped",
|
"executionsList.showMessage.stopExecution.title": "Execution stopped",
|
||||||
"executionsList.startedAt": "Started At",
|
"executionsList.startedAt": "Started At",
|
||||||
|
"executionsList.startingSoon": "Starting soon",
|
||||||
"executionsList.started": "{date} at {time}",
|
"executionsList.started": "{date} at {time}",
|
||||||
"executionsList.id": "Execution ID",
|
"executionsList.id": "Execution ID",
|
||||||
"executionsList.status": "Status",
|
"executionsList.status": "Status",
|
||||||
|
|
|
@ -22,3 +22,7 @@ export function convertToDisplayDate(fullDate: Date | string | number): {
|
||||||
const [date, time] = formattedDate.split('#');
|
const [date, time] = formattedDate.split('#');
|
||||||
return { date, time };
|
return { date, time };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toDayMonth = (fullDate: Date | string) => dateformat(fullDate, 'd mmm');
|
||||||
|
|
||||||
|
export const toTime = (fullDate: Date | string) => dateformat(fullDate, 'HH:MM:ss');
|
||||||
|
|
|
@ -2122,6 +2122,7 @@ export interface IWorkflowBase {
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
startedAt?: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
nodes: INode[];
|
nodes: INode[];
|
||||||
connections: IConnections;
|
connections: IConnections;
|
||||||
|
@ -2463,6 +2464,7 @@ export interface ExecutionSummary {
|
||||||
retryOf?: string | null;
|
retryOf?: string | null;
|
||||||
retrySuccessId?: string | null;
|
retrySuccessId?: string | null;
|
||||||
waitTill?: Date;
|
waitTill?: Date;
|
||||||
|
createdAt?: Date;
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
stoppedAt?: Date;
|
stoppedAt?: Date;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
|
Loading…
Reference in a new issue