From d78a41db5420ff6711c30899aa2d71a85049374c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 23 Mar 2023 18:07:46 +0100 Subject: [PATCH] feat: Execution custom data saving and filtering (#5496) * wip: workflow execution filtering * fix: import type failing to build * fix: remove console.logs * feat: execution metadata migrations * fix(editor): Move global executions filter to its own component * fix(editor): Using the same filter component in workflow level * fix(editor): a small housekeeping * checking workflowId in filter applied * fix(editor): update filter after resolving merge conflicts * fix(editor): unify empy filter status * feat(editor): add datetime picker to filter * feat(editor): add meta fields * fix: fix button override in datepicker panel * feat(editor): add filter metadata * feat(core): add 'startedBefore' execution filter prop * feat(core): add 'tags' execution query filter * Revert "feat(core): add 'tags' execution query filter" This reverts commit a7b968081c91290b0c94df18c6a73d29950222d9. * feat(editor): add translations and tooltip and counting selected filter props * fix(editor): fix label layouts * fix(editor): update custom data docs link * fix(editor): update custom data tooltip position * fix(editor): update tooltip text * refactor: Ignore metadata if not enabled by license * fix(editor): Add paywall states to advanced execution filter * refactor: Save custom data also for worker mode * fix: Remove duplicate migration name from list * fix(editor): Reducing filter complexity and add debounce to text inputs * fix(editor): Remove unused import, add comment * fix(editor): simplify event listener * fix: Prevent error when there are running executions * test(editor): Add advanced execution filter basic unit test * test(editor): Add advanced execution filter state change unit test * fix: Small lint issue * feat: Add indices to speed up queries * feat: add customData limits * refactor: put metadata save in transaction * chore: remove unneed comment * test: add tests for execution metadata * fix(editor): Fixes after merge conflict * fix(editor): Remove unused import * wordings and ui fixes * fix(editor): type fixes * feat: add code node autocompletions for customData * fix: Prevent transaction issues and ambiguous ID in sql clauses * fix(editor): Suppress requesting current executions if metadata is used in filter (#5739) * fix(editor): Suppress requesting current executions if metadata is used in filter * fix(editor): Fix arrows for select in popover * refactor: Improve performance by correcting database indices * fix: Lint issue * test: Fix broken test * fix: Broken test * test: add call data check for saveExecutionMetadata test --------- Co-authored-by: Valya Bullions Co-authored-by: Alex Grozav Co-authored-by: Omar Ajoue Co-authored-by: Romain Minaud --- packages/cli/src/Db.ts | 2 + packages/cli/src/Interfaces.ts | 2 + .../cli/src/WorkflowExecuteAdditionalData.ts | 33 ++ .../src/databases/entities/ExecutionEntity.ts | 6 +- .../databases/entities/ExecutionMetadata.ts | 22 + packages/cli/src/databases/entities/index.ts | 2 + ...9416281779-CreateExecutionMetadataTable.ts | 69 +++ .../src/databases/migrations/mysqldb/index.ts | 2 + ...9416281778-CreateExecutionMetadataTable.ts | 62 +++ .../databases/migrations/postgresdb/index.ts | 2 + ...9416281777-CreateExecutionMetadataTable.ts | 64 +++ .../src/databases/migrations/sqlite/index.ts | 2 + .../cli/src/executions/executions.service.ts | 103 ++++- .../WorkflowExecuteAdditionalData.test.ts | 41 ++ packages/core/src/NodeExecuteFunctions.ts | 49 +- .../core/src/WorkflowExecutionMetadata.ts | 44 ++ .../test/WorkflowExecutionMetadata.test.ts | 165 +++++++ packages/editor-ui/src/Interface.ts | 30 +- .../completions/execution.completions.ts | 33 ++ .../src/components/ExecutionFilter.vue | 418 ++++++++++++++++++ .../src/components/ExecutionsList.vue | 182 +++----- .../ExecutionsView/ExecutionsList.vue | 47 +- .../ExecutionsView/ExecutionsSidebar.vue | 97 +--- .../__tests__/ExecutionFilter.test.ts | 130 ++++++ .../__tests__/ExecutionsList.test.ts | 10 + packages/editor-ui/src/constants.ts | 1 + .../src/plugins/i18n/locales/en.json | 22 +- packages/editor-ui/src/stores/workflows.ts | 8 +- .../editor-ui/src/utils/executionUtils.ts | 50 +++ packages/workflow/src/Interfaces.ts | 1 + 30 files changed, 1430 insertions(+), 269 deletions(-) create mode 100644 packages/cli/src/databases/entities/ExecutionMetadata.ts create mode 100644 packages/cli/src/databases/migrations/mysqldb/1679416281779-CreateExecutionMetadataTable.ts create mode 100644 packages/cli/src/databases/migrations/postgresdb/1679416281778-CreateExecutionMetadataTable.ts create mode 100644 packages/cli/src/databases/migrations/sqlite/1679416281777-CreateExecutionMetadataTable.ts create mode 100644 packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts create mode 100644 packages/core/src/WorkflowExecutionMetadata.ts create mode 100644 packages/core/test/WorkflowExecutionMetadata.test.ts create mode 100644 packages/editor-ui/src/components/ExecutionFilter.vue create mode 100644 packages/editor-ui/src/components/__tests__/ExecutionFilter.test.ts create mode 100644 packages/editor-ui/src/utils/executionUtils.ts diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 68ff6a0adaf38..28b760fd2e618 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -169,6 +169,8 @@ export async function init( collections.InstalledPackages = linkRepository(entities.InstalledPackages); collections.InstalledNodes = linkRepository(entities.InstalledNodes); collections.WorkflowStatistics = linkRepository(entities.WorkflowStatistics); + collections.ExecutionMetadata = linkRepository(entities.ExecutionMetadata); + collections.EventDestinations = linkRepository(entities.EventDestinations); isInitialized = true; diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index d801e5e3cc0a0..991b055f9fd92 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -48,6 +48,7 @@ import type { WebhookEntity } from '@db/entities/WebhookEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import type { EventDestinations } from '@db/entities/MessageEventBusDestinationEntity'; +import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata'; export interface IActivationError { time: number; @@ -88,6 +89,7 @@ export interface IDatabaseCollections { InstalledNodes: Repository; WorkflowStatistics: Repository; EventDestinations: Repository; + ExecutionMetadata: Repository; } // ---------------------------------- diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 6c8cb06db4631..0e2ad0c7ee40b 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -71,6 +71,7 @@ import { PermissionChecker } from './UserManagement/PermissionChecker'; import { WorkflowsService } from './workflows/workflows.services'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; +import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -264,6 +265,22 @@ async function pruneExecutionData(this: WorkflowHooks): Promise { } } +export async function saveExecutionMetadata( + executionId: string, + executionMetadata: Record, +): Promise { + const metadataRows = []; + for (const [key, value] of Object.entries(executionMetadata)) { + metadataRows.push({ + execution: { id: executionId }, + key, + value, + }); + } + + return Db.collections.ExecutionMetadata.save(metadataRows); +} + /** * Returns hook functions to push data to Editor-UI * @@ -657,6 +674,14 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { executionData as IExecutionFlattedDb, ); + try { + if (fullRunData.data.resultData.metadata) { + await saveExecutionMetadata(this.executionId, fullRunData.data.resultData.metadata); + } + } catch (e) { + Logger.error(`Failed to save metadata for execution ID ${this.executionId}`, e); + } + if (fullRunData.finished === true && this.retryOf !== undefined) { // If the retry was successful save the reference it on the original execution // await Db.collections.Execution.save(executionData as IExecutionFlattedDb); @@ -789,6 +814,14 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { status: executionData.status, }); + try { + if (fullRunData.data.resultData.metadata) { + await saveExecutionMetadata(this.executionId, fullRunData.data.resultData.metadata); + } + } catch (e) { + Logger.error(`Failed to save metadata for execution ID ${this.executionId}`, e); + } + if (fullRunData.finished === true && this.retryOf !== undefined) { // If the retry was successful save the reference it on the original execution await Db.collections.Execution.update(this.retryOf, { diff --git a/packages/cli/src/databases/entities/ExecutionEntity.ts b/packages/cli/src/databases/entities/ExecutionEntity.ts index 2172f2700366a..a29ef9da0e991 100644 --- a/packages/cli/src/databases/entities/ExecutionEntity.ts +++ b/packages/cli/src/databases/entities/ExecutionEntity.ts @@ -1,9 +1,10 @@ import { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow'; -import { Column, Entity, Generated, Index, PrimaryColumn } from 'typeorm'; +import { Column, Entity, Generated, Index, OneToMany, PrimaryColumn } from 'typeorm'; import { datetimeColumnType, jsonColumnType } from './AbstractEntity'; import { IWorkflowDb } from '@/Interfaces'; import type { IExecutionFlattedDb } from '@/Interfaces'; import { idStringifier } from '../utils/transformers'; +import type { ExecutionMetadata } from './ExecutionMetadata'; @Entity() @Index(['workflowId', 'id']) @@ -49,4 +50,7 @@ export class ExecutionEntity implements IExecutionFlattedDb { @Column({ type: datetimeColumnType, nullable: true }) waitTill: Date; + + @OneToMany('ExecutionMetadata', 'execution') + metadata: ExecutionMetadata[]; } diff --git a/packages/cli/src/databases/entities/ExecutionMetadata.ts b/packages/cli/src/databases/entities/ExecutionMetadata.ts new file mode 100644 index 0000000000000..99ea8e01cec24 --- /dev/null +++ b/packages/cli/src/databases/entities/ExecutionMetadata.ts @@ -0,0 +1,22 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'; +import { ExecutionEntity } from './ExecutionEntity'; + +@Entity() +export class ExecutionMetadata { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne('ExecutionEntity', 'metadata', { + onDelete: 'CASCADE', + }) + execution: ExecutionEntity; + + @RelationId((executionMetadata: ExecutionMetadata) => executionMetadata.execution) + executionId: number; + + @Column('text') + key: string; + + @Column('text') + value: string; +} diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index 17ff382044afc..55ba7b865183e 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -15,6 +15,7 @@ import { User } from './User'; import { WebhookEntity } from './WebhookEntity'; import { WorkflowEntity } from './WorkflowEntity'; import { WorkflowStatistics } from './WorkflowStatistics'; +import { ExecutionMetadata } from './ExecutionMetadata'; export const entities = { AuthIdentity, @@ -33,4 +34,5 @@ export const entities = { WebhookEntity, WorkflowEntity, WorkflowStatistics, + ExecutionMetadata, }; diff --git a/packages/cli/src/databases/migrations/mysqldb/1679416281779-CreateExecutionMetadataTable.ts b/packages/cli/src/databases/migrations/mysqldb/1679416281779-CreateExecutionMetadataTable.ts new file mode 100644 index 0000000000000..89a53d340e280 --- /dev/null +++ b/packages/cli/src/databases/migrations/mysqldb/1679416281779-CreateExecutionMetadataTable.ts @@ -0,0 +1,69 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; + +export class CreateExecutionMetadataTable1679416281779 implements MigrationInterface { + name = 'CreateExecutionMetadataTable1679416281779'; + + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = getTablePrefix(); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}execution_metadata ( + id int(11) auto_increment NOT NULL PRIMARY KEY, + executionId int(11) NOT NULL, + \`key\` TEXT NOT NULL, + value TEXT NOT NULL, + CONSTRAINT \`${tablePrefix}execution_metadata_FK\` FOREIGN KEY (\`executionId\`) REFERENCES \`${tablePrefix}execution_entity\` (\`id\`) ON DELETE CASCADE, + INDEX \`IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb\` (\`executionId\` ASC) + ) + ENGINE=InnoDB`, + ); + + // Remove indices that are no longer needed since the addition of the status column + await queryRunner.query( + `DROP INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_${tablePrefix}cefb067df2402f6aed0638a6c1\` ON \`${tablePrefix}execution_entity\``, + ); + + // Add index to the new status column + await queryRunner.query( + `CREATE INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\` (\`status\`, \`workflowId\`)`, + ); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = getTablePrefix(); + + await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`); + await queryRunner.query( + `CREATE INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`waitTill\`, \`id\`)`, + ); + await queryRunner.query( + `CREATE INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`finished\`, \`id\`)`, + ); + await queryRunner.query( + `CREATE INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\` (\`finished\`, \`id\`)`, + ); + await queryRunner.query( + 'CREATE INDEX `IDX_' + + tablePrefix + + 'cefb067df2402f6aed0638a6c1` ON `' + + tablePrefix + + 'execution_entity` (`stoppedAt`)', + ); + await queryRunner.query( + `DROP INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\``, + ); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index b67441e08f26c..bb021d495b800 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -34,6 +34,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus'; import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus'; +import { CreateExecutionMetadataTable1679416281779 } from './1679416281779-CreateExecutionMetadataTable'; export const mysqlMigrations = [ InitialMigration1588157391238, @@ -72,4 +73,5 @@ export const mysqlMigrations = [ AddStatusToExecutions1674138566000, MigrateExecutionStatus1676996103000, UpdateRunningExecutionStatus1677236788851, + CreateExecutionMetadataTable1679416281779, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/1679416281778-CreateExecutionMetadataTable.ts b/packages/cli/src/databases/migrations/postgresdb/1679416281778-CreateExecutionMetadataTable.ts new file mode 100644 index 0000000000000..384d3f2c03c74 --- /dev/null +++ b/packages/cli/src/databases/migrations/postgresdb/1679416281778-CreateExecutionMetadataTable.ts @@ -0,0 +1,62 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; + +export class CreateExecutionMetadataTable1679416281778 implements MigrationInterface { + name = 'CreateExecutionMetadataTable1679416281778'; + + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = getTablePrefix(); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}execution_metadata ( + "id" serial4 NOT NULL PRIMARY KEY, + "executionId" int4 NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + CONSTRAINT ${tablePrefix}execution_metadata_fk FOREIGN KEY ("executionId") REFERENCES ${tablePrefix}execution_entity(id) ON DELETE CASCADE + )`, + ); + + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb" ON "${tablePrefix}execution_metadata" ("executionId");`, + ); + + // Remove indices that are no longer needed since the addition of the status column + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}33228da131bb1112247cf52a42"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}72ffaaab9f04c2c1f1ea86e662"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}58154df94c686818c99fb754ce"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}4f474ac92be81610439aaad61e"`); + + // Create new index for status + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584" ON "${tablePrefix}execution_entity" ("status", "workflowId");`, + ); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = getTablePrefix(); + + // Re-add removed indices + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}33228da131bb1112247cf52a42" ON ${tablePrefix}execution_entity ("stoppedAt") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}72ffaaab9f04c2c1f1ea86e662" ON ${tablePrefix}execution_entity ("finished", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}58154df94c686818c99fb754ce" ON ${tablePrefix}execution_entity ("workflowId", "waitTill", "id") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}4f474ac92be81610439aaad61e" ON ${tablePrefix}execution_entity ("workflowId", "finished", "id") `, + ); + + await queryRunner.query( + `DROP INDEX IF EXISTS "IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584"`, + ); + + await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`); + } +} diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 9aa75b16ed1db..0c0c33ca38624 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -32,6 +32,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus'; import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus'; +import { CreateExecutionMetadataTable1679416281778 } from './1679416281778-CreateExecutionMetadataTable'; export const postgresMigrations = [ InitialMigration1587669153312, @@ -68,4 +69,5 @@ export const postgresMigrations = [ AddStatusToExecutions1674138566000, MigrateExecutionStatus1676996103000, UpdateRunningExecutionStatus1677236854063, + CreateExecutionMetadataTable1679416281778, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1679416281777-CreateExecutionMetadataTable.ts b/packages/cli/src/databases/migrations/sqlite/1679416281777-CreateExecutionMetadataTable.ts new file mode 100644 index 0000000000000..39c39ff522662 --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1679416281777-CreateExecutionMetadataTable.ts @@ -0,0 +1,64 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; + +export class CreateExecutionMetadataTable1679416281777 implements MigrationInterface { + name = 'CreateExecutionMetadataTable1679416281777'; + + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = getTablePrefix(); + + await queryRunner.query( + `CREATE TABLE "${tablePrefix}execution_metadata" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + executionId INTEGER NOT NULL, + "key" TEXT NOT NULL, + value TEXT NOT NULL, + CONSTRAINT ${tablePrefix}execution_metadata_entity_FK FOREIGN KEY (executionId) REFERENCES ${tablePrefix}execution_entity(id) ON DELETE CASCADE + )`, + ); + + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb" ON "${tablePrefix}execution_metadata" ("executionId");`, + ); + + // Re add some lost indices from migration DeleteExecutionsWithWorkflows.ts + // that were part of AddExecutionEntityIndexes.ts + // not all were needed since we added the `status` column to execution_entity + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9' ON '${tablePrefix}execution_entity' ('waitTill', 'id') `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4' ON '${tablePrefix}execution_entity' ('workflowId', 'id') `, + ); + + // Also add index to the new status column + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584' ON '${tablePrefix}execution_entity' ('status', 'workflowId') `, + ); + + // Remove no longer needed index to waitTill since it's already covered by the index b94b45ce2c73ce46c54f20b5f9 above + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2'`); + // Remove index for stoppedAt since it's not used anymore + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}cefb067df2402f6aed0638a6c1'`); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = getTablePrefix(); + + await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9'`); + await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4'`); + await queryRunner.query( + `DROP INDEX IF EXISTS 'IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584'`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`, + ); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 42782c2430c7b..2d1d3e6709201 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -31,6 +31,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus'; import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus'; +import { CreateExecutionMetadataTable1679416281777 } from './1679416281777-CreateExecutionMetadataTable'; const sqliteMigrations = [ InitialMigration1588102412422, @@ -66,6 +67,7 @@ const sqliteMigrations = [ AddStatusToExecutions1674138566000, MigrateExecutionStatus1676996103000, UpdateRunningExecutionStatus1677237073720, + CreateExecutionMetadataTable1679416281777, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/executions/executions.service.ts b/packages/cli/src/executions/executions.service.ts index d087c7acfdb09..6165d13b2a125 100644 --- a/packages/cli/src/executions/executions.service.ts +++ b/packages/cli/src/executions/executions.service.ts @@ -14,7 +14,7 @@ import type { } from 'n8n-workflow'; import { deepCopy, LoggerProxy, jsonParse, Workflow } from 'n8n-workflow'; import type { FindOperator, FindOptionsWhere } from 'typeorm'; -import { In, IsNull, LessThanOrEqual, Not, Raw } from 'typeorm'; +import { In, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, Raw } from 'typeorm'; import { ActiveExecutions } from '@/ActiveExecutions'; import config from '@/config'; import type { User } from '@db/entities/User'; @@ -35,10 +35,15 @@ import * as Db from '@/Db'; import * as GenericHelpers from '@/GenericHelpers'; import { parse } from 'flatted'; import { Container } from 'typedi'; -import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers'; +import { + getStatusUsingPreviousExecutionStatusMethod, + isAdvancedExecutionFiltersEnabled, +} from './executionHelpers'; +import { ExecutionMetadata } from '@/databases/entities/ExecutionMetadata'; +import { DateUtils } from 'typeorm/util/DateUtils'; interface IGetExecutionsQueryFilter { - id?: FindOperator; + id?: FindOperator | string; finished?: boolean; mode?: string; retryOf?: string; @@ -47,12 +52,16 @@ interface IGetExecutionsQueryFilter { workflowId?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any waitTill?: FindOperator | boolean; + metadata?: Array<{ key: string; value: string }>; + startedAfter?: string; + startedBefore?: string; } const schemaGetExecutionsQueryFilter = { $id: '/IGetExecutionsQueryFilter', type: 'object', properties: { + id: { type: 'string' }, finished: { type: 'boolean' }, mode: { type: 'string' }, retryOf: { type: 'string' }, @@ -63,6 +72,21 @@ const schemaGetExecutionsQueryFilter = { }, waitTill: { type: 'boolean' }, workflowId: { anyOf: [{ type: 'integer' }, { type: 'string' }] }, + metadata: { type: 'array', items: { $ref: '#/$defs/metadata' } }, + startedAfter: { type: 'date-time' }, + startedBefore: { type: 'date-time' }, + }, + $defs: { + metadata: { + type: 'object', + required: ['key', 'value'], + properties: { + key: { + type: 'string', + }, + value: { type: 'string' }, + }, + }, }, }; @@ -84,17 +108,38 @@ export class ExecutionsService { static async getExecutionsCount( countFilter: IDataObject, user: User, + metadata?: Array<{ key: string; value: string }>, ): Promise<{ count: number; estimated: boolean }> { const dbType = config.getEnv('database.type'); const filteredFields = Object.keys(countFilter).filter((field) => field !== 'id'); // For databases other than Postgres, do a regular count // when filtering based on `workflowId` or `finished` fields. - if (dbType !== 'postgresdb' || filteredFields.length > 0 || user.globalRole.name !== 'owner') { + if ( + dbType !== 'postgresdb' || + metadata?.length || + filteredFields.length > 0 || + user.globalRole.name !== 'owner' + ) { const sharedWorkflowIds = await this.getWorkflowIdsForUser(user); - const countParams = { where: { workflowId: In(sharedWorkflowIds), ...countFilter } }; - const count = await Db.collections.Execution.count(countParams); + let query = Db.collections.Execution.createQueryBuilder('execution') + .select() + .orderBy('execution.id', 'DESC') + .where({ workflowId: In(sharedWorkflowIds) }); + + if (metadata?.length) { + query = query.leftJoinAndSelect(ExecutionMetadata, 'md', 'md.executionId = execution.id'); + for (const md of metadata) { + query = query.andWhere('md.key = :key AND md.value = :value', md); + } + } + + if (filteredFields.length > 0) { + query = query.andWhere(countFilter); + } + + const count = await query.getCount(); return { count, estimated: false }; } @@ -138,6 +183,18 @@ export class ExecutionsService { } else { delete filter.waitTill; } + + if (Array.isArray(filter.metadata)) { + delete filter.metadata; + } + + if ('startedAfter' in filter) { + delete filter.startedAfter; + } + + if ('startedBefore' in filter) { + delete filter.startedBefore; + } } } @@ -227,17 +284,17 @@ export class ExecutionsService { } = {}; if (req.query.lastId) { - rangeQuery.push('id < :lastId'); + rangeQuery.push('execution.id < :lastId'); rangeQueryParams.lastId = req.query.lastId; } if (req.query.firstId) { - rangeQuery.push('id > :firstId'); + rangeQuery.push('execution.id > :firstId'); rangeQueryParams.firstId = req.query.firstId; } if (executingWorkflowIds.length > 0) { - rangeQuery.push('id NOT IN (:...executingWorkflowIds)'); + rangeQuery.push('execution.id NOT IN (:...executingWorkflowIds)'); rangeQueryParams.executingWorkflowIds = executingWorkflowIds; } @@ -261,11 +318,36 @@ export class ExecutionsService { 'execution.workflowData', 'execution.status', ]) - .orderBy('id', 'DESC') + .orderBy('execution.id', 'DESC') .take(limit) .where(findWhere); const countFilter = deepCopy(filter ?? {}); + const metadata = isAdvancedExecutionFiltersEnabled() ? filter?.metadata : undefined; + + if (metadata?.length) { + query = query.leftJoin(ExecutionMetadata, 'md', 'md.executionId = execution.id'); + for (const md of metadata) { + query = query.andWhere('md.key = :key AND md.value = :value', md); + } + } + + if (filter?.startedAfter) { + query = query.andWhere({ + startedAt: MoreThanOrEqual( + DateUtils.mixedDateToUtcDatetimeString(new Date(filter.startedAfter)), + ), + }); + } + + if (filter?.startedBefore) { + query = query.andWhere({ + startedAt: LessThanOrEqual( + DateUtils.mixedDateToUtcDatetimeString(new Date(filter.startedBefore)), + ), + }); + } + // deepcopy breaks the In operator so we need to reapply it if (filter?.status) { Object.assign(filter, { status: In(filter.status) }); @@ -285,6 +367,7 @@ export class ExecutionsService { const { count, estimated } = await this.getExecutionsCount( countFilter as IDataObject, req.user, + metadata, ); const formattedExecutions: IExecutionsSummary[] = executions.map((execution) => { diff --git a/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts b/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts new file mode 100644 index 0000000000000..9d364bd788939 --- /dev/null +++ b/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts @@ -0,0 +1,41 @@ +import { saveExecutionMetadata } from '@/WorkflowExecuteAdditionalData'; +import * as Db from '@/Db'; +import { mocked } from 'jest-mock'; + +jest.mock('@/Db', () => { + return { + collections: { + ExecutionMetadata: { + save: jest.fn(async () => Promise.resolve([])), + }, + }, + }; +}); + +describe('WorkflowExecuteAdditionalData', () => { + test('Execution metadata is saved in a batch', async () => { + const toSave = { + test1: 'value1', + test2: 'value2', + }; + const executionId = '1234'; + + await saveExecutionMetadata(executionId, toSave); + + expect(mocked(Db.collections.ExecutionMetadata.save)).toHaveBeenCalledTimes(1); + expect(mocked(Db.collections.ExecutionMetadata.save).mock.calls[0]).toEqual([ + [ + { + execution: { id: executionId }, + key: 'test1', + value: 'value1', + }, + { + execution: { id: executionId }, + key: 'test2', + value: 'value2', + }, + ], + ]); + }); +}); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index b95b05bebd15a..147cc61fb999d 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -112,6 +112,12 @@ import { extractValue } from './ExtractValue'; import { getClientCredentialsToken } from './OAuth2Helper'; import { PLACEHOLDER_EMPTY_EXECUTION_ID } from './Constants'; import { binaryToBuffer } from './BinaryDataManager/utils'; +import { + getAllWorkflowExecutionMetadata, + getWorkflowExecutionMetadata, + setAllWorkflowExecutionMetadata, + setWorkflowExecutionMetadata, +} from './WorkflowExecutionMetadata'; axios.defaults.timeout = 300000; // Prevent axios from adding x-form-www-urlencoded headers by default @@ -1616,6 +1622,7 @@ export async function requestWithAuthentication( export function getAdditionalKeys( additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, + runExecutionData: IRunExecutionData | null, ): IWorkflowDataProxyAdditionalKeys { const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID; const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`; @@ -1624,6 +1631,22 @@ export function getAdditionalKeys( id: executionId, mode: mode === 'manual' ? 'test' : 'production', resumeUrl, + customData: runExecutionData + ? { + set(key: string, value: string): void { + setWorkflowExecutionMetadata(runExecutionData, key, value); + }, + setAll(obj: Record): void { + setAllWorkflowExecutionMetadata(runExecutionData, obj); + }, + get(key: string): string { + return getWorkflowExecutionMetadata(runExecutionData, key); + }, + getAll(): Record { + return getAllWorkflowExecutionMetadata(runExecutionData); + }, + } + : undefined, }, // deprecated @@ -2122,7 +2145,7 @@ export function getExecutePollFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), undefined, fallbackValue, options, @@ -2181,7 +2204,7 @@ export function getExecuteTriggerFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), undefined, fallbackValue, options, @@ -2240,7 +2263,7 @@ export function getExecuteFunctions( connectionInputData, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, ); }, @@ -2306,7 +2329,7 @@ export function getExecuteFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, fallbackValue, options, @@ -2323,7 +2346,7 @@ export function getExecuteFunctions( {}, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, ); return dataProxy.getDataProxy(); @@ -2413,7 +2436,7 @@ export function getExecuteSingleFunctions( connectionInputData, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, ); }, @@ -2484,7 +2507,7 @@ export function getExecuteSingleFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, fallbackValue, options, @@ -2501,7 +2524,7 @@ export function getExecuteSingleFunctions( {}, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), executeData, ); return dataProxy.getDataProxy(); @@ -2594,7 +2617,7 @@ export function getLoadOptionsFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), undefined, fallbackValue, options, @@ -2643,7 +2666,7 @@ export function getExecuteHookFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, runExecutionData), undefined, fallbackValue, options, @@ -2657,7 +2680,7 @@ export function getExecuteHookFunctions( additionalData, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, null), isTest, ); }, @@ -2720,7 +2743,7 @@ export function getExecuteWebhookFunctions( itemIndex, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, null), undefined, fallbackValue, options, @@ -2758,7 +2781,7 @@ export function getExecuteWebhookFunctions( additionalData, mode, additionalData.timezone, - getAdditionalKeys(additionalData, mode), + getAdditionalKeys(additionalData, mode, null), ), getWebhookName: () => webhookData.webhookDescription.name, prepareOutputData: NodeHelpers.prepareOutputData, diff --git a/packages/core/src/WorkflowExecutionMetadata.ts b/packages/core/src/WorkflowExecutionMetadata.ts new file mode 100644 index 0000000000000..146d5418d20d3 --- /dev/null +++ b/packages/core/src/WorkflowExecutionMetadata.ts @@ -0,0 +1,44 @@ +import type { IRunExecutionData } from 'n8n-workflow'; + +export const KV_LIMIT = 10; + +export function setWorkflowExecutionMetadata( + executionData: IRunExecutionData, + key: string, + value: unknown, +) { + if (!executionData.resultData.metadata) { + executionData.resultData.metadata = {}; + } + // Currently limited to 10 metadata KVs + if ( + !(key in executionData.resultData.metadata) && + Object.keys(executionData.resultData.metadata).length >= KV_LIMIT + ) { + return; + } + executionData.resultData.metadata[String(key).slice(0, 50)] = String(value).slice(0, 255); +} + +export function setAllWorkflowExecutionMetadata( + executionData: IRunExecutionData, + obj: Record, +) { + Object.entries(obj).forEach(([key, value]) => + setWorkflowExecutionMetadata(executionData, key, value), + ); +} + +export function getAllWorkflowExecutionMetadata( + executionData: IRunExecutionData, +): Record { + // Make a copy so it can't be modified directly + return { ...executionData.resultData.metadata } ?? {}; +} + +export function getWorkflowExecutionMetadata( + executionData: IRunExecutionData, + key: string, +): string { + return getAllWorkflowExecutionMetadata(executionData)[String(key).slice(0, 50)]; +} diff --git a/packages/core/test/WorkflowExecutionMetadata.test.ts b/packages/core/test/WorkflowExecutionMetadata.test.ts new file mode 100644 index 0000000000000..1c1ee49bf288b --- /dev/null +++ b/packages/core/test/WorkflowExecutionMetadata.test.ts @@ -0,0 +1,165 @@ +import { + getAllWorkflowExecutionMetadata, + getWorkflowExecutionMetadata, + KV_LIMIT, + setAllWorkflowExecutionMetadata, + setWorkflowExecutionMetadata, +} from '@/WorkflowExecutionMetadata'; +import type { IRunExecutionData } from 'n8n-workflow'; + +describe('Execution Metadata functions', () => { + test('setWorkflowExecutionMetadata will set a value', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + setWorkflowExecutionMetadata(executionData, 'test1', 'value1'); + + expect(metadata).toEqual({ + test1: 'value1', + }); + }); + + test('setAllWorkflowExecutionMetadata will set multiple values', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + setAllWorkflowExecutionMetadata(executionData, { + test1: 'value1', + test2: 'value2', + }); + + expect(metadata).toEqual({ + test1: 'value1', + test2: 'value2', + }); + }); + + test('setWorkflowExecutionMetadata should convert values to strings', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + setWorkflowExecutionMetadata(executionData, 'test1', 1234); + + expect(metadata).toEqual({ + test1: '1234', + }); + }); + + test('setWorkflowExecutionMetadata should limit the number of metadata entries', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + const expected: Record = {}; + for (let i = 0; i < KV_LIMIT; i++) { + expected[`test${i + 1}`] = `value${i + 1}`; + } + + for (let i = 0; i < KV_LIMIT + 10; i++) { + setWorkflowExecutionMetadata(executionData, `test${i + 1}`, `value${i + 1}`); + } + + expect(metadata).toEqual(expected); + }); + + test('getWorkflowExecutionMetadata should return a single value for an existing key', () => { + const metadata: Record = { test1: 'value1' }; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + expect(getWorkflowExecutionMetadata(executionData, 'test1')).toBe('value1'); + }); + + test('getWorkflowExecutionMetadata should return undefined for an unset key', () => { + const metadata: Record = { test1: 'value1' }; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + expect(getWorkflowExecutionMetadata(executionData, 'test2')).toBeUndefined(); + }); + + test('getAllWorkflowExecutionMetadata should return all metadata', () => { + const metadata: Record = { test1: 'value1', test2: 'value2' }; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + expect(getAllWorkflowExecutionMetadata(executionData)).toEqual(metadata); + }); + + test('getAllWorkflowExecutionMetadata should not an object that modifies internal state', () => { + const metadata: Record = { test1: 'value1', test2: 'value2' }; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + getAllWorkflowExecutionMetadata(executionData).test1 = 'changed'; + + expect(metadata.test1).not.toBe('changed'); + expect(metadata.test1).toBe('value1'); + }); + + test('setWorkflowExecutionMetadata should truncate long keys', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + setWorkflowExecutionMetadata( + executionData, + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + 'value1', + ); + + expect(metadata).toEqual({ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 'value1', + }); + }); + + test('setWorkflowExecutionMetadata should truncate long values', () => { + const metadata = {}; + const executionData = { + resultData: { + metadata, + }, + } as IRunExecutionData; + + setWorkflowExecutionMetadata( + executionData, + 'test1', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + ); + + expect(metadata).toEqual({ + test1: + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }); + }); +}); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index d0394414a20f9..a97a4b4050171 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -137,9 +137,9 @@ export interface IExternalHooks { export interface IRestApi { getActiveWorkflows(): Promise; getActivationError(id: string): Promise; - getCurrentExecutions(filter: IDataObject): Promise; + getCurrentExecutions(filter: ExecutionsQueryFilter): Promise; getPastExecutions( - filter: IDataObject, + filter: ExecutionsQueryFilter, limit: number, lastId?: string, firstId?: string, @@ -393,7 +393,7 @@ export interface IExecutionsStopData { export interface IExecutionDeleteFilter { deleteBefore?: Date; - filters?: IDataObject; + filters?: ExecutionsQueryFilter; ids?: string[]; } @@ -1455,3 +1455,27 @@ export type NodeAuthenticationOption = { value: string; displayOptions?: IDisplayOptions; }; + +export type ExecutionFilterMetadata = { + key: string; + value: string; +}; + +export type ExecutionFilterType = { + status: string; + workflowId: string; + startDate: string | Date; + endDate: string | Date; + tags: string[]; + metadata: ExecutionFilterMetadata[]; +}; + +export type ExecutionsQueryFilter = { + status?: ExecutionStatus[]; + workflowId?: string; + finished?: boolean; + waitTill?: boolean; + metadata?: Array<{ key: string; value: string }>; + startedAfter?: string; + startedBefore?: string; +}; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/execution.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/execution.completions.ts index cb48a3436b3ec..558e32196097b 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/execution.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/execution.completions.ts @@ -18,6 +18,15 @@ export const executionCompletions = (Vue as CodeNodeEditorMixin).extend({ if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null; + const buildLinkNode = (text: string) => { + const wrapper = document.createElement('span'); + // This is being loaded from the locales file. This could + // cause an XSS of some kind but multiple other locales strings + // do the same thing. + wrapper.innerHTML = text; + return () => wrapper; + }; + const options: Completion[] = [ { label: `${matcher}.id`, @@ -31,6 +40,30 @@ export const executionCompletions = (Vue as CodeNodeEditorMixin).extend({ label: `${matcher}.resumeUrl`, info: this.$locale.baseText('codeNodeEditor.completer.$execution.resumeUrl'), }, + { + label: `${matcher}.customData.set("key", "value")`, + info: buildLinkNode( + this.$locale.baseText('codeNodeEditor.completer.$execution.customData.set()'), + ), + }, + { + label: `${matcher}.customData.get("key")`, + info: buildLinkNode( + this.$locale.baseText('codeNodeEditor.completer.$execution.customData.get()'), + ), + }, + { + label: `${matcher}.customData.setAll({})`, + info: buildLinkNode( + this.$locale.baseText('codeNodeEditor.completer.$execution.customData.setAll()'), + ), + }, + { + label: `${matcher}.customData.getAll()`, + info: buildLinkNode( + this.$locale.baseText('codeNodeEditor.completer.$execution.customData.getAll()'), + ), + }, ]; return { diff --git a/packages/editor-ui/src/components/ExecutionFilter.vue b/packages/editor-ui/src/components/ExecutionFilter.vue new file mode 100644 index 0000000000000..5cc1244954827 --- /dev/null +++ b/packages/editor-ui/src/components/ExecutionFilter.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index 589b5bd605c8e..b29b7a3abfe1c 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -1,43 +1,19 @@