From 8b2b78d441ca0d479bc7cb7c59b6d77761213e7e Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Wed, 7 Aug 2024 17:59:58 +0200 Subject: [PATCH 01/79] wip: Execution annotation entities and migration --- packages/cli/src/databases/config.ts | 1 + .../databases/entities/AnnotationTagEntity.ts | 21 +++++++++ .../entities/AnnotationTagMapping.ts | 20 ++++++++ .../databases/entities/ExecutionAnnotation.ts | 26 +++++++++++ .../src/databases/entities/ExecutionEntity.ts | 4 ++ ...9529455-CreateExecutionAnnotationTables.ts | 46 +++++++++++++++++++ .../src/databases/migrations/mysqldb/index.ts | 2 + .../databases/migrations/postgresdb/index.ts | 2 + ...690000000002-MigrateIntegerKeysToString.ts | 1 + .../src/databases/migrations/sqlite/index.ts | 2 + .../repositories/execution.repository.ts | 2 + 11 files changed, 127 insertions(+) create mode 100644 packages/cli/src/databases/entities/AnnotationTagEntity.ts create mode 100644 packages/cli/src/databases/entities/AnnotationTagMapping.ts create mode 100644 packages/cli/src/databases/entities/ExecutionAnnotation.ts create mode 100644 packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts diff --git a/packages/cli/src/databases/config.ts b/packages/cli/src/databases/config.ts index aa6c0a570a834..1f8c748fa7afe 100644 --- a/packages/cli/src/databases/config.ts +++ b/packages/cli/src/databases/config.ts @@ -62,6 +62,7 @@ const getSqliteConnectionOptions = (): SqliteConnectionOptions | SqlitePooledCon database: path.resolve(Container.get(InstanceSettings).n8nFolder, sqliteConfig.database), migrations: sqliteMigrations, }; + if (sqliteConfig.poolSize > 0) { return { type: 'sqlite-pooled', diff --git a/packages/cli/src/databases/entities/AnnotationTagEntity.ts b/packages/cli/src/databases/entities/AnnotationTagEntity.ts new file mode 100644 index 0000000000000..1fdd91b4360f6 --- /dev/null +++ b/packages/cli/src/databases/entities/AnnotationTagEntity.ts @@ -0,0 +1,21 @@ +import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm'; +import { IsString, Length } from 'class-validator'; +import type { WorkflowEntity } from './WorkflowEntity'; +import type { WorkflowTagMapping } from './WorkflowTagMapping'; +import { WithTimestampsAndStringId } from './AbstractEntity'; +import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; + +@Entity() +export class AnnotationTagEntity extends WithTimestampsAndStringId { + @Column({ length: 24 }) + @Index({ unique: true }) + @IsString({ message: 'Tag name must be of type string.' }) + @Length(1, 24, { message: 'Tag name must be $constraint1 to $constraint2 characters long.' }) + name: string; + + @ManyToMany('ExecutionAnnotation', 'tags') + annotations: ExecutionAnnotation[]; + + @OneToMany('AnnotationTagMapping', 'tags') + annotationMappings: WorkflowTagMapping[]; +} diff --git a/packages/cli/src/databases/entities/AnnotationTagMapping.ts b/packages/cli/src/databases/entities/AnnotationTagMapping.ts new file mode 100644 index 0000000000000..93b8471ec2477 --- /dev/null +++ b/packages/cli/src/databases/entities/AnnotationTagMapping.ts @@ -0,0 +1,20 @@ +import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; +import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; +import { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; + +@Entity({ name: 'executions_annotations_tags' }) +export class WorkflowTagMapping { + @PrimaryColumn() + annotationId: string; + + @ManyToOne('AnnotationEntity', 'tagMappings') + @JoinColumn({ name: 'annotationId' }) + annotations: ExecutionAnnotation[]; + + @PrimaryColumn() + tagId: string; + + @ManyToOne('AnnotationTagEntity', 'annotationMappings') + @JoinColumn({ name: 'tagId' }) + tags: AnnotationTagEntity[]; +} diff --git a/packages/cli/src/databases/entities/ExecutionAnnotation.ts b/packages/cli/src/databases/entities/ExecutionAnnotation.ts new file mode 100644 index 0000000000000..b3b5dd89d86de --- /dev/null +++ b/packages/cli/src/databases/entities/ExecutionAnnotation.ts @@ -0,0 +1,26 @@ +import { Column, Entity, Generated, ManyToOne, PrimaryColumn, RelationId } from '@n8n/typeorm'; +import { idStringifier } from '../utils/transformers'; +import { ExecutionEntity } from './ExecutionEntity'; + +type AnnotationVote = 'up' | 'down'; + +@Entity() +export class ExecutionAnnotation { + @Generated() + @PrimaryColumn({ transformer: idStringifier }) + id: string; + + @Column({ type: 'varchar', nullable: true }) + vote: AnnotationVote; + + @Column({ type: 'varchar', nullable: true }) + note: string; + + @RelationId((annotation: ExecutionAnnotation) => annotation.execution) + executionId: string; + + @ManyToOne('ExecutionEntity', 'data', { + onDelete: 'CASCADE', + }) + execution: ExecutionEntity; +} diff --git a/packages/cli/src/databases/entities/ExecutionEntity.ts b/packages/cli/src/databases/entities/ExecutionEntity.ts index dbd597a82869f..3190b83cabd28 100644 --- a/packages/cli/src/databases/entities/ExecutionEntity.ts +++ b/packages/cli/src/databases/entities/ExecutionEntity.ts @@ -16,6 +16,7 @@ import { idStringifier } from '../utils/transformers'; import type { ExecutionData } from './ExecutionData'; import type { ExecutionMetadata } from './ExecutionMetadata'; import { WorkflowEntity } from './WorkflowEntity'; +import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; @Entity() @Index(['workflowId', 'id']) @@ -65,6 +66,9 @@ export class ExecutionEntity { @OneToOne('ExecutionData', 'execution') executionData: Relation; + @OneToOne('ExecutionAnnotation', 'execution') + annotation: Relation; + @ManyToOne('WorkflowEntity') workflow: WorkflowEntity; } diff --git a/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts new file mode 100644 index 0000000000000..b26c45ce38196 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts @@ -0,0 +1,46 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +const annotationsTableName = 'execution_annotations'; +const annotationTagsTableName = 'annotation_tag_entity'; +const annotationTagMappingsTableName = 'execution_annotation_tags'; + +export class CreateAnnotationTables1723039529455 implements ReversibleMigration { + async up({ schemaBuilder: { createTable, column } }: MigrationContext) { + await createTable(annotationsTableName) + .withColumns( + column('id').int.notNull.primary.autoGenerate, + column('executionId').int.notNull, + column('vote').varchar(6), + column('note').text, + ) + .withForeignKey('executionId', { + tableName: 'execution_entity', + columnName: 'id', + onDelete: 'CASCADE', + }).withTimestamps; + + await createTable(annotationTagsTableName).withColumns( + column('id').varchar(24).primary.notNull, + column('name').varchar(24).notNull, + ).withTimestamps; + + await createTable(annotationTagMappingsTableName) + .withColumns(column('annotationId').int.notNull, column('tagId').varchar(24).notNull) + .withForeignKey('annotationId', { + tableName: annotationsTableName, + columnName: 'id', + onDelete: 'CASCADE', + }) + .withForeignKey('tagId', { + tableName: annotationTagsTableName, + columnName: 'id', + onDelete: 'CASCADE', + }); + } + + async down({ schemaBuilder: { dropTable } }: MigrationContext) { + await dropTable(annotationsTableName); + await dropTable(annotationTagsTableName); + await dropTable(annotationTagMappingsTableName); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index ecd5f66a7c257..9591b1d016a17 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -59,6 +59,7 @@ import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNo import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable'; import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; +import { CreateAnnotationTables1723039529455 } from '../common/1723039529455-CreateExecutionAnnotationTables'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -121,4 +122,5 @@ export const mysqlMigrations: Migration[] = [ MakeExecutionStatusNonNullable1714133768521, AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, + CreateAnnotationTables1723039529455, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 720c79a8e35d9..176afa1dd3308 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -59,6 +59,7 @@ import { MakeExecutionStatusNonNullable1714133768521 } from '../common/171413376 import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; import { FixExecutionMetadataSequence1721377157740 } from './1721377157740-FixExecutionMetadataSequence'; +import { CreateAnnotationTables1723039529455 } from '../common/1723039529455-CreateExecutionAnnotationTables'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -121,4 +122,5 @@ export const postgresMigrations: Migration[] = [ AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, FixExecutionMetadataSequence1721377157740, + CreateAnnotationTables1723039529455, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts index ab2da63cd58b8..99be9a670b4a6 100644 --- a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts +++ b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts @@ -195,6 +195,7 @@ function getSqliteDbFileSize(): number { Container.get(InstanceSettings).n8nFolder, Container.get(GlobalConfig).database.sqlite.database, ); + const { size } = statSync(filename); return size; } diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 15000a78e0458..5ecaade860653 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -56,6 +56,7 @@ import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNo import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable'; import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; +import { CreateAnnotationTables1723039529455 } from '../common/1723039529455-CreateExecutionAnnotationTables'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -115,6 +116,7 @@ const sqliteMigrations: Migration[] = [ MakeExecutionStatusNonNullable1714133768521, AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, + CreateAnnotationTables1723039529455, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 5b4e515af6da3..2126550e51214 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -328,6 +328,8 @@ export class ExecutionRepository extends Repository { await this.update({ id: executionId }, executionInformation); } + console.log(data); + if (data || workflowData) { const executionData: Partial = {}; if (workflowData) { From bb364fca4ab15b5b87277986b2d7d2d5d2599312 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Thu, 8 Aug 2024 20:08:41 +0200 Subject: [PATCH 02/79] wip: add indexes --- .../1723039529455-CreateExecutionAnnotationTables.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts index b26c45ce38196..5d3135c34caff 100644 --- a/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts +++ b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts @@ -5,7 +5,7 @@ const annotationTagsTableName = 'annotation_tag_entity'; const annotationTagMappingsTableName = 'execution_annotation_tags'; export class CreateAnnotationTables1723039529455 implements ReversibleMigration { - async up({ schemaBuilder: { createTable, column } }: MigrationContext) { + async up({ schemaBuilder: { createTable, column }, tablePrefix }: MigrationContext) { await createTable(annotationsTableName) .withColumns( column('id').int.notNull.primary.autoGenerate, @@ -13,6 +13,7 @@ export class CreateAnnotationTables1723039529455 implements ReversibleMigration column('vote').varchar(6), column('note').text, ) + .withIndexOn('executionId') .withForeignKey('executionId', { tableName: 'execution_entity', columnName: 'id', @@ -31,6 +32,8 @@ export class CreateAnnotationTables1723039529455 implements ReversibleMigration columnName: 'id', onDelete: 'CASCADE', }) + .withIndexOn('tagId') + .withIndexOn('annotationId') .withForeignKey('tagId', { tableName: annotationTagsTableName, columnName: 'id', @@ -39,8 +42,9 @@ export class CreateAnnotationTables1723039529455 implements ReversibleMigration } async down({ schemaBuilder: { dropTable } }: MigrationContext) { - await dropTable(annotationsTableName); - await dropTable(annotationTagsTableName); + console.log('DOWN DOWN DOWN'); await dropTable(annotationTagMappingsTableName); + await dropTable(annotationTagsTableName); + await dropTable(annotationsTableName); } } From 6fe3d7ebfe326904f922e276033437f2f0f20a64 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Thu, 8 Aug 2024 20:09:37 +0200 Subject: [PATCH 03/79] wip: typeorm entities --- .../databases/entities/AnnotationTagEntity.ts | 3 +- .../entities/AnnotationTagMapping.ts | 6 +-- .../databases/entities/ExecutionAnnotation.ts | 42 +++++++++++++++---- .../src/databases/entities/ExecutionEntity.ts | 2 +- packages/cli/src/databases/entities/index.ts | 6 +++ packages/cli/test/integration/shared/types.ts | 1 + 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/databases/entities/AnnotationTagEntity.ts b/packages/cli/src/databases/entities/AnnotationTagEntity.ts index 1fdd91b4360f6..5fffa4a9a5a08 100644 --- a/packages/cli/src/databases/entities/AnnotationTagEntity.ts +++ b/packages/cli/src/databases/entities/AnnotationTagEntity.ts @@ -1,9 +1,8 @@ import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm'; import { IsString, Length } from 'class-validator'; -import type { WorkflowEntity } from './WorkflowEntity'; import type { WorkflowTagMapping } from './WorkflowTagMapping'; import { WithTimestampsAndStringId } from './AbstractEntity'; -import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; +import type { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; @Entity() export class AnnotationTagEntity extends WithTimestampsAndStringId { diff --git a/packages/cli/src/databases/entities/AnnotationTagMapping.ts b/packages/cli/src/databases/entities/AnnotationTagMapping.ts index 93b8471ec2477..e50d44c9cc34a 100644 --- a/packages/cli/src/databases/entities/AnnotationTagMapping.ts +++ b/packages/cli/src/databases/entities/AnnotationTagMapping.ts @@ -2,12 +2,12 @@ import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; import { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; -@Entity({ name: 'executions_annotations_tags' }) -export class WorkflowTagMapping { +@Entity({ name: 'execution_annotation_tags' }) +export class AnnotationTagMapping { @PrimaryColumn() annotationId: string; - @ManyToOne('AnnotationEntity', 'tagMappings') + @ManyToOne('ExecutionAnnotation', 'tagMappings') @JoinColumn({ name: 'annotationId' }) annotations: ExecutionAnnotation[]; diff --git a/packages/cli/src/databases/entities/ExecutionAnnotation.ts b/packages/cli/src/databases/entities/ExecutionAnnotation.ts index b3b5dd89d86de..093e289e01792 100644 --- a/packages/cli/src/databases/entities/ExecutionAnnotation.ts +++ b/packages/cli/src/databases/entities/ExecutionAnnotation.ts @@ -1,14 +1,23 @@ -import { Column, Entity, Generated, ManyToOne, PrimaryColumn, RelationId } from '@n8n/typeorm'; -import { idStringifier } from '../utils/transformers'; +import { + Column, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + RelationId, +} from '@n8n/typeorm'; import { ExecutionEntity } from './ExecutionEntity'; +import type { AnnotationTagEntity } from './AnnotationTagEntity'; +import type { AnnotationTagMapping } from './AnnotationTagMapping'; -type AnnotationVote = 'up' | 'down'; +export type AnnotationVote = 'up' | 'down'; -@Entity() +@Entity({ name: 'execution_annotations' }) export class ExecutionAnnotation { - @Generated() - @PrimaryColumn({ transformer: idStringifier }) - id: string; + @PrimaryGeneratedColumn() + id: number; @Column({ type: 'varchar', nullable: true }) vote: AnnotationVote; @@ -19,8 +28,25 @@ export class ExecutionAnnotation { @RelationId((annotation: ExecutionAnnotation) => annotation.execution) executionId: string; - @ManyToOne('ExecutionEntity', 'data', { + @ManyToOne('ExecutionEntity', 'annotation', { onDelete: 'CASCADE', }) execution: ExecutionEntity; + + @ManyToMany('AnnotationTagEntity', 'annotations') + @JoinTable({ + name: 'execution_annotation_tags', // table name for the junction table of this relation + joinColumn: { + name: 'annotationId', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'tagId', + referencedColumnName: 'id', + }, + }) + tags?: AnnotationTagEntity[]; + + @OneToMany('AnnotationTagMapping', 'annotations') + tagMappings: AnnotationTagMapping[]; } diff --git a/packages/cli/src/databases/entities/ExecutionEntity.ts b/packages/cli/src/databases/entities/ExecutionEntity.ts index 3190b83cabd28..9e480a3fc7cf5 100644 --- a/packages/cli/src/databases/entities/ExecutionEntity.ts +++ b/packages/cli/src/databases/entities/ExecutionEntity.ts @@ -67,7 +67,7 @@ export class ExecutionEntity { executionData: Relation; @OneToOne('ExecutionAnnotation', 'execution') - annotation: Relation; + annotation?: Relation; @ManyToOne('WorkflowEntity') workflow: WorkflowEntity; diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index db8b113baf394..c7ab20b84affa 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -21,13 +21,19 @@ import { ExecutionData } from './ExecutionData'; import { WorkflowHistory } from './WorkflowHistory'; import { Project } from './Project'; import { ProjectRelation } from './ProjectRelation'; +import { AnnotationTagEntity } from './AnnotationTagEntity'; +import { AnnotationTagMapping } from './AnnotationTagMapping'; +import { ExecutionAnnotation } from './ExecutionAnnotation'; export const entities = { + AnnotationTagEntity, + AnnotationTagMapping, AuthIdentity, AuthProviderSyncHistory, AuthUser, CredentialsEntity, EventDestinations, + ExecutionAnnotation, ExecutionEntity, InstalledNodes, InstalledPackages, diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index cb794d0f95b92..33f18c4dc1e79 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -26,6 +26,7 @@ type EndpointGroup = | 'eventBus' | 'license' | 'variables' + | 'annotationTags' | 'tags' | 'externalSecrets' | 'mfa' From 5b1adeaadeec8fdaa9909dd9e7f52f493b8586eb Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Thu, 8 Aug 2024 20:10:18 +0200 Subject: [PATCH 04/79] wip: annotation tag endpoint --- packages/@n8n/permissions/src/types.ts | 3 + packages/cli/src/GenericHelpers.ts | 2 + packages/cli/src/Server.ts | 1 + .../controllers/annotationTags.controller.ts | 45 ++++++++++ .../repositories/annotationTag.repository.ts | 75 ++++++++++++++++ packages/cli/src/permissions/global-roles.ts | 10 +++ packages/cli/src/requests.ts | 11 +++ .../cli/src/services/annotationTag.service.ts | 86 +++++++++++++++++++ .../integration/shared/utils/testServer.ts | 4 + packages/workflow/src/Interfaces.ts | 7 ++ 10 files changed, 244 insertions(+) create mode 100644 packages/cli/src/controllers/annotationTags.controller.ts create mode 100644 packages/cli/src/databases/repositories/annotationTag.repository.ts create mode 100644 packages/cli/src/services/annotationTag.service.ts diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index 2720272e6fd75..3f43240f91253 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -1,5 +1,6 @@ export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list'; export type Resource = + | 'annotationTag' | 'auditLogs' | 'banner' | 'communityPackage' @@ -28,6 +29,7 @@ export type ResourceScope< export type WildcardScope = `${Resource}:*` | '*'; +export type AnnotationTagScope = ResourceScope<'annotationTag'>; export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>; export type BannerScope = ResourceScope<'banner', 'dismiss'>; export type CommunityPackageScope = ResourceScope< @@ -62,6 +64,7 @@ export type WorkflowScope = ResourceScope< >; export type Scope = + | AnnotationTagScope | AuditLogsScope | BannerScope | CommunityPackageScope diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index 13762e5dfd672..24a27f9a3ad29 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -1,6 +1,7 @@ import { validate } from 'class-validator'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; +import type { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import type { UserRoleChangePayload, UserUpdatePayload } from '@/requests'; @@ -11,6 +12,7 @@ export async function validateEntity( | WorkflowEntity | CredentialsEntity | TagEntity + | AnnotationTagEntity | User | UserUpdatePayload | UserRoleChangePayload, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 0eb2040e7f1bb..dd7127d146d5b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -38,6 +38,7 @@ import { OrchestrationService } from '@/services/orchestration.service'; import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; import '@/controllers/activeWorkflows.controller'; +import '@/controllers/annotationTags.controller'; import '@/controllers/auth.controller'; import '@/controllers/binaryData.controller'; import '@/controllers/curl.controller'; diff --git a/packages/cli/src/controllers/annotationTags.controller.ts b/packages/cli/src/controllers/annotationTags.controller.ts new file mode 100644 index 0000000000000..ea9896f785125 --- /dev/null +++ b/packages/cli/src/controllers/annotationTags.controller.ts @@ -0,0 +1,45 @@ +import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators'; +import { AnnotationTagService } from '@/services/annotationTag.service'; +import { AnnotationTagsRequest } from '@/requests'; + +@RestController('/annotation-tags') +export class AnnotationTagsController { + constructor(private readonly annotationTagService: AnnotationTagService) {} + + @Get('/') + @GlobalScope('annotationTag:list') + async getAll(req: AnnotationTagsRequest.GetAll) { + return await this.annotationTagService.getAll({ + withUsageCount: req.query.withUsageCount === 'true', + }); + } + + @Post('/') + @GlobalScope('annotationTag:create') + async createTag(req: AnnotationTagsRequest.Create) { + const tag = this.annotationTagService.toEntity({ name: req.body.name }); + + return await this.annotationTagService.save(tag, 'create'); + } + + @Patch('/:id(\\w+)') + @GlobalScope('annotationTag:update') + async updateTag(req: AnnotationTagsRequest.Update) { + const newTag = this.annotationTagService.toEntity({ + id: req.params.id, + name: req.body.name.trim(), + }); + + return await this.annotationTagService.save(newTag, 'update'); + } + + @Delete('/:id(\\w+)') + @GlobalScope('annotationTag:delete') + async deleteTag(req: AnnotationTagsRequest.Delete) { + const { id } = req.params; + + await this.annotationTagService.delete(id); + + return true; + } +} diff --git a/packages/cli/src/databases/repositories/annotationTag.repository.ts b/packages/cli/src/databases/repositories/annotationTag.repository.ts new file mode 100644 index 0000000000000..5a3dd048fa410 --- /dev/null +++ b/packages/cli/src/databases/repositories/annotationTag.repository.ts @@ -0,0 +1,75 @@ +import { Service } from 'typedi'; +import type { EntityManager } from '@n8n/typeorm'; +import { DataSource, In, Repository } from '@n8n/typeorm'; +import intersection from 'lodash/intersection'; +import { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; +import type { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; + +@Service() +export class AnnotationTagRepository extends Repository { + constructor(dataSource: DataSource) { + super(AnnotationTagEntity, dataSource.manager); + } + + async findMany(tagIds: string[]) { + return await this.find({ + select: ['id', 'name'], + where: { id: In(tagIds) }, + }); + } + + /** + * Set tags on execution annotation to import while ensuring all tags exist in the database, + * either by matching incoming to existing tags or by creating them first. + */ + async setTags(tx: EntityManager, dbTags: AnnotationTagEntity[], annotation: ExecutionAnnotation) { + if (!annotation?.tags?.length) return; + + for (let i = 0; i < annotation.tags.length; i++) { + const importTag = annotation.tags[i]; + + if (!importTag.name) continue; + + const identicalMatch = dbTags.find( + (dbTag) => + dbTag.id === importTag.id && + dbTag.createdAt && + importTag.createdAt && + dbTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(), + ); + + if (identicalMatch) { + annotation.tags[i] = identicalMatch; + continue; + } + + const nameMatch = dbTags.find((dbTag) => dbTag.name === importTag.name); + + if (nameMatch) { + annotation.tags[i] = nameMatch; + continue; + } + + const tagEntity = this.create(importTag); + + annotation.tags[i] = await tx.save(tagEntity); + } + } + + /** + * Returns the annotation IDs that have certain tags. + * Intersection! e.g. annotation needs to have all provided tags. + */ + async getAnnotationIdsViaTags(tags: string[]): Promise { + const dbTags = await this.find({ + where: { name: In(tags) }, + relations: ['annotations'], + }); + + const annotationIdsPerTag = dbTags.map((tag) => + tag.annotations.map((annotation) => annotation.id), + ); + + return intersection(...annotationIdsPerTag); + } +} diff --git a/packages/cli/src/permissions/global-roles.ts b/packages/cli/src/permissions/global-roles.ts index ad930dfdd21d8..664cd8384ea7e 100644 --- a/packages/cli/src/permissions/global-roles.ts +++ b/packages/cli/src/permissions/global-roles.ts @@ -1,6 +1,11 @@ import type { Scope } from '@n8n/permissions'; export const GLOBAL_OWNER_SCOPES: Scope[] = [ + 'annotationTag:create', + 'annotationTag:read', + 'annotationTag:update', + 'annotationTag:delete', + 'annotationTag:list', 'auditLogs:manage', 'banner:dismiss', 'credential:create', @@ -75,6 +80,11 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [ export const GLOBAL_ADMIN_SCOPES = GLOBAL_OWNER_SCOPES.concat(); export const GLOBAL_MEMBER_SCOPES: Scope[] = [ + 'annotationTag:create', + 'annotationTag:read', + 'annotationTag:update', + 'annotationTag:delete', + 'annotationTag:list', 'eventBusDestination:list', 'eventBusDestination:test', 'tag:create', diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 4fe369857eb7a..2e64c0a751816 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -431,6 +431,17 @@ export declare namespace TagsRequest { type Delete = AuthenticatedRequest<{ id: string }>; } +// ---------------------------------- +// /annotation-tags +// ---------------------------------- + +export declare namespace AnnotationTagsRequest { + type GetAll = AuthenticatedRequest<{}, {}, {}, { withUsageCount: string }>; + type Create = AuthenticatedRequest<{}, {}, { name: string }>; + type Update = AuthenticatedRequest<{ id: string }, {}, { name: string }>; + type Delete = AuthenticatedRequest<{ id: string }>; +} + // ---------------------------------- // /nodes // ---------------------------------- diff --git a/packages/cli/src/services/annotationTag.service.ts b/packages/cli/src/services/annotationTag.service.ts new file mode 100644 index 0000000000000..8e01d3f18249a --- /dev/null +++ b/packages/cli/src/services/annotationTag.service.ts @@ -0,0 +1,86 @@ +import { Service } from 'typedi'; +import { validateEntity } from '@/GenericHelpers'; +import type { ITagWithCountDb } from '@/Interfaces'; +import type { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; +import { AnnotationTagRepository } from '@db/repositories/annotationTag.repository'; +import { ExternalHooks } from '@/ExternalHooks'; + +type GetAllResult = T extends { withUsageCount: true } + ? ITagWithCountDb[] + : AnnotationTagEntity[]; + +@Service() +export class AnnotationTagService { + constructor( + private externalHooks: ExternalHooks, + private tagRepository: AnnotationTagRepository, + ) {} + + toEntity(attrs: { name: string; id?: string }) { + attrs.name = attrs.name.trim(); + + return this.tagRepository.create(attrs); + } + + async save(tag: AnnotationTagEntity, actionKind: 'create' | 'update') { + await validateEntity(tag); + + const action = actionKind[0].toUpperCase() + actionKind.slice(1); + + await this.externalHooks.run(`annotationTag.before${action}`, [tag]); + + const savedTag = this.tagRepository.save(tag, { transaction: false }); + + await this.externalHooks.run(`annotationTag.after${action}`, [tag]); + + return await savedTag; + } + + async delete(id: string) { + await this.externalHooks.run('annotationTag.beforeDelete', [id]); + + const deleteResult = this.tagRepository.delete(id); + + await this.externalHooks.run('annotationTag.afterDelete', [id]); + + return await deleteResult; + } + + async getAll(options?: T): Promise> { + if (options?.withUsageCount) { + const allTags = await this.tagRepository.find({ + select: ['id', 'name', 'createdAt', 'updatedAt'], + relations: ['annotationMappings'], + }); + + return allTags.map(({ annotationMappings, ...rest }) => { + return { + ...rest, + usageCount: annotationMappings.length, + } as ITagWithCountDb; + }) as GetAllResult; + } + + return await (this.tagRepository.find({ + select: ['id', 'name', 'createdAt', 'updatedAt'], + }) as Promise>); + } + + async getById(id: string) { + return await this.tagRepository.findOneOrFail({ + where: { id }, + }); + } + + /** + * Sort tags based on the order of the tag IDs in the request. + */ + sortByRequestOrder(tags: AnnotationTagEntity[], { requestOrder }: { requestOrder: string[] }) { + const tagMap = tags.reduce>((acc, tag) => { + acc[tag.id] = tag; + return acc; + }, {}); + + return requestOrder.map((tagId) => tagMap[tagId]); + } +} diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 7776b7e669415..ff83794f9bb75 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -124,6 +124,10 @@ export const setupTestServer = ({ if (endpointGroups.length) { for (const group of endpointGroups) { switch (group) { + case 'annotationTags': + await import('@/controllers/annotationTags.controller'); + break; + case 'credentials': await import('@/credentials/credentials.controller'); break; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 39b4d06b47d98..5d22da977e947 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2443,6 +2443,13 @@ export interface ExecutionSummary { nodeExecutionStatus?: { [key: string]: IExecutionSummaryNodeExecutionResult; }; + annotation?: { + vote: AnnotationVote; + tags: Array<{ + id: string; + name: string; + }>; + }; } export interface IExecutionSummaryNodeExecutionResult { From 94a0d35d54a24c5826fce4e091598ab7ebaf9521 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Thu, 8 Aug 2024 20:10:37 +0200 Subject: [PATCH 05/79] wip: adding annotation tags to executions endpoint --- .../repositories/execution.repository.ts | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 2126550e51214..aa0820b5f85a9 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -46,6 +46,8 @@ import { Logger } from '@/Logger'; import type { ExecutionSummaries } from '@/executions/execution.types'; import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error'; import { separate } from '@/utils'; +import { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; +import { AnnotationTagMapping } from '@db/entities/AnnotationTagMapping'; export interface IGetExecutionsQueryFilter { id?: FindOperator | string; @@ -685,12 +687,38 @@ export class ExecutionRepository extends Repository { stoppedAt: true, }; + private annotationFields = { + id: true, + vote: true, + }; + async findManyByRangeQuery(query: ExecutionSummaries.RangeQuery): Promise { if (query?.accessibleWorkflowIds?.length === 0) { throw new ApplicationError('Expected accessible workflow IDs'); } - const executions: ExecutionSummary[] = await this.toQueryBuilder(query).getRawMany(); + const qb = this.toQueryBuilderWithAnnotationTags(query); + + const rawExecutionsWithTags: ExecutionSummary[] = await qb.getRawMany(); + + // FIXME: This needs refactoring + const executions = rawExecutionsWithTags.reduce( + (acc, { tagId, tagName, ...row }) => { + const existingExecution = acc.find((e) => e.id === row.id); + if (existingExecution) { + if (tagId) { + existingExecution.tags.push({ id: tagId as string, name: tagName as string }); + } + } else { + acc.push({ + ...row, + tags: tagId ? [{ id: tagId as string, name: tagName as string }] : [], + }); + } + return acc; + }, + [] as Array }>, + ); return executions.map((execution) => this.toSummary(execution)); } @@ -768,14 +796,20 @@ export class ExecutionRepository extends Repository { metadata, } = query; + const annotationFields = Object.keys(this.annotationFields).map( + (key) => `annotation.${key} AS "annotation.${key}"`, + ); + const fields = Object.keys(this.summaryFields) .concat(['waitTill', 'retrySuccessId']) .map((key) => `execution.${key} AS "${key}"`) - .concat('workflow.name AS "workflowName"'); + .concat('workflow.name AS "workflowName"') + .concat(annotationFields); const qb = this.createQueryBuilder('execution') .select(fields) .innerJoin('execution.workflow', 'workflow') + .leftJoin('execution.annotation', 'annotation') .where('execution.workflowId IN (:...accessibleWorkflowIds)', { accessibleWorkflowIds }); if (query.kind === 'range') { @@ -817,6 +851,18 @@ export class ExecutionRepository extends Repository { return qb; } + private toQueryBuilderWithAnnotationTags(query: ExecutionSummaries.Query) { + const subQuery = this.toQueryBuilder(query); + + return this.manager + .createQueryBuilder() + .select(['e.*', 'ate.id AS "tagId"', 'ate.name AS "tagName"']) + .from(`(${subQuery.getQuery()})`, 'e') + .setParameters(subQuery.getParameters()) + .leftJoin(AnnotationTagMapping, 'atm', 'atm.annotationId = e."annotation.id"') + .leftJoin(AnnotationTagEntity, 'ate', 'ate.id = atm.tagId'); + } + async getAllIds() { const executions = await this.find({ select: ['id'], order: { id: 'ASC' } }); From 27020f45e07bf1079d601824d62156e0d8f5f3c2 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Fri, 9 Aug 2024 09:59:25 +0200 Subject: [PATCH 06/79] wip: show annotation tags in executions list --- .../components/executions/workflow/WorkflowExecutionsCard.vue | 3 +++ packages/editor-ui/src/composables/useExecutionHelpers.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue index 4c84a270425b9..60f9ec61eec43 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue @@ -56,6 +56,7 @@ {{ $locale.baseText('executionDetails.retry') }} #{{ execution.retryOf }} +
; } export function useExecutionHelpers() { @@ -20,6 +21,7 @@ export function useExecutionHelpers() { label: 'Status unknown', runningTime: '', showTimestamp: true, + tags: execution.tags, }; if (execution.status === 'new') { From 8eed096c5948f2dfbf16deb9cce934b055402867 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Fri, 9 Aug 2024 15:46:13 +0200 Subject: [PATCH 07/79] wip: list of executions filtered by annotation tags --- .../repositories/execution.repository.ts | 47 ++++++++++++++----- .../cli/src/executions/execution.service.ts | 1 + .../cli/src/executions/execution.types.ts | 1 + .../src/executions/executions.controller.ts | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index aa0820b5f85a9..8ac8de77cdef9 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -330,8 +330,6 @@ export class ExecutionRepository extends Repository { await this.update({ id: executionId }, executionInformation); } - console.log(data); - if (data || workflowData) { const executionData: Partial = {}; if (workflowData) { @@ -697,7 +695,8 @@ export class ExecutionRepository extends Repository { throw new ApplicationError('Expected accessible workflow IDs'); } - const qb = this.toQueryBuilderWithAnnotationTags(query); + const qb = this.toQueryBuilderWithAnnotations(query); + console.log(qb.getQuery()); const rawExecutionsWithTags: ExecutionSummary[] = await qb.getRawMany(); @@ -794,22 +793,17 @@ export class ExecutionRepository extends Repository { startedBefore, startedAfter, metadata, + tags, } = query; - const annotationFields = Object.keys(this.annotationFields).map( - (key) => `annotation.${key} AS "annotation.${key}"`, - ); - const fields = Object.keys(this.summaryFields) .concat(['waitTill', 'retrySuccessId']) .map((key) => `execution.${key} AS "${key}"`) - .concat('workflow.name AS "workflowName"') - .concat(annotationFields); + .concat('workflow.name AS "workflowName"'); const qb = this.createQueryBuilder('execution') .select(fields) .innerJoin('execution.workflow', 'workflow') - .leftJoin('execution.annotation', 'annotation') .where('execution.workflowId IN (:...accessibleWorkflowIds)', { accessibleWorkflowIds }); if (query.kind === 'range') { @@ -848,11 +842,40 @@ export class ExecutionRepository extends Repository { qb.setParameter('value', value); } + if (tags?.length) { + // If there is a filter by one or multiple tags, we need to join the annotations table + qb.innerJoin('execution.annotation', 'annotation'); + + // Add an inner join for each tag + for (const tag of tags) { + qb.innerJoin( + AnnotationTagMapping, + `atm_${tag}`, + `atm_${tag}.annotationId = annotation.id AND atm_${tag}.tagId = :tagId_${tag}`, + ); + + qb.setParameter(`tagId_${tag}`, tag); + } + } + return qb; } - private toQueryBuilderWithAnnotationTags(query: ExecutionSummaries.Query) { - const subQuery = this.toQueryBuilder(query); + // This method is used to add the annotation fields to the executions query + // It uses original query builder as a subquery and adds the annotation fields to it + private toQueryBuilderWithAnnotations(query: ExecutionSummaries.Query) { + const annotationFields = Object.keys(this.annotationFields).map( + (key) => `annotation.${key} AS "annotation.${key}"`, + ); + + const subQuery = this.toQueryBuilder(query).addSelect(annotationFields); + + // Ensure the join with annotations is made only once + // It might be already present as an inner join if the query includes tags filter + // If not, it must be added as a left join + if (!subQuery.expressionMap.joinAttributes.some((join) => join.alias.name === 'annotation')) { + subQuery.leftJoin('execution.annotation', 'annotation'); + } return this.manager .createQueryBuilder() diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index bb8650e99f1d6..73e010fc53d2b 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -59,6 +59,7 @@ export const schemaGetExecutionsQueryFilter = { metadata: { type: 'array', items: { $ref: '#/$defs/metadata' } }, startedAfter: { type: 'date-time' }, startedBefore: { type: 'date-time' }, + tags: { type: 'array', items: { type: 'string' } }, }, $defs: { metadata: { diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts index 7e8872bf1b305..7d5ffe393e57a 100644 --- a/packages/cli/src/executions/execution.types.ts +++ b/packages/cli/src/executions/execution.types.ts @@ -63,6 +63,7 @@ export namespace ExecutionSummaries { metadata: Array<{ key: string; value: string }>; startedAfter: string; startedBefore: string; + tags: string[]; // tag IDs }>; type AccessFields = { diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 64b6a5427429f..81ec60bd9a1d1 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -47,7 +47,7 @@ export class ExecutionsController { query.accessibleWorkflowIds = accessibleWorkflowIds; - if (!this.license.isAdvancedExecutionFiltersEnabled()) delete query.metadata; + // if (!this.license.isAdvancedExecutionFiltersEnabled()) delete query.metadata; const noStatus = !query.status || query.status.length === 0; const noRange = !query.range.lastId || !query.range.firstId; From 7aa5e6019bf49a55d6b01890be847d6516b6e3ad Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Fri, 9 Aug 2024 15:47:06 +0200 Subject: [PATCH 08/79] wip: remove temporary workaround --- packages/cli/src/executions/executions.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 81ec60bd9a1d1..64b6a5427429f 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -47,7 +47,7 @@ export class ExecutionsController { query.accessibleWorkflowIds = accessibleWorkflowIds; - // if (!this.license.isAdvancedExecutionFiltersEnabled()) delete query.metadata; + if (!this.license.isAdvancedExecutionFiltersEnabled()) delete query.metadata; const noStatus = !query.status || query.status.length === 0; const noRange = !query.range.lastId || !query.range.firstId; From afb7501b6029200ddba5c43e64c271e3c24e7626 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Mon, 12 Aug 2024 12:51:26 +0200 Subject: [PATCH 09/79] wip: endpoint for annotating an execution --- packages/cli/src/Interfaces.ts | 6 +++ .../databases/entities/AnnotationTagEntity.ts | 4 +- .../entities/AnnotationTagMapping.ts | 6 +-- .../databases/entities/ExecutionAnnotation.ts | 3 +- ...9529455-CreateExecutionAnnotationTables.ts | 2 +- .../annotationTagMapping.repository.ts | 33 +++++++++++++++ .../repositories/execution.repository.ts | 41 ++++++++++++++++--- .../cli/src/executions/execution.service.ts | 10 +++++ .../cli/src/executions/execution.types.ts | 7 ++++ .../src/executions/executions.controller.ts | 23 ++++++++++- packages/workflow/src/Interfaces.ts | 2 + 11 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 packages/cli/src/databases/repositories/annotationTagMapping.repository.ts diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 8ddbd10fb29c1..de56ab0688825 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -146,6 +146,12 @@ export interface IExecutionResponse extends IExecutionBase { retrySuccessId?: string; workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials; customData: Record; + annotation: { + tags: Array<{ + id: string; + name: string; + }>; + }; } // Flatted data to save memory when saving in database or transferring diff --git a/packages/cli/src/databases/entities/AnnotationTagEntity.ts b/packages/cli/src/databases/entities/AnnotationTagEntity.ts index 5fffa4a9a5a08..bf54843ec9b40 100644 --- a/packages/cli/src/databases/entities/AnnotationTagEntity.ts +++ b/packages/cli/src/databases/entities/AnnotationTagEntity.ts @@ -1,8 +1,8 @@ import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm'; import { IsString, Length } from 'class-validator'; -import type { WorkflowTagMapping } from './WorkflowTagMapping'; import { WithTimestampsAndStringId } from './AbstractEntity'; import type { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; +import type { AnnotationTagMapping } from '@db/entities/AnnotationTagMapping'; @Entity() export class AnnotationTagEntity extends WithTimestampsAndStringId { @@ -16,5 +16,5 @@ export class AnnotationTagEntity extends WithTimestampsAndStringId { annotations: ExecutionAnnotation[]; @OneToMany('AnnotationTagMapping', 'tags') - annotationMappings: WorkflowTagMapping[]; + annotationMappings: AnnotationTagMapping[]; } diff --git a/packages/cli/src/databases/entities/AnnotationTagMapping.ts b/packages/cli/src/databases/entities/AnnotationTagMapping.ts index e50d44c9cc34a..251a2dcaa5a3d 100644 --- a/packages/cli/src/databases/entities/AnnotationTagMapping.ts +++ b/packages/cli/src/databases/entities/AnnotationTagMapping.ts @@ -1,11 +1,11 @@ import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; -import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; -import { AnnotationTagEntity } from '@db/entities/AnnotationTagEntity'; +import type { ExecutionAnnotation } from './ExecutionAnnotation'; +import type { AnnotationTagEntity } from './AnnotationTagEntity'; @Entity({ name: 'execution_annotation_tags' }) export class AnnotationTagMapping { @PrimaryColumn() - annotationId: string; + annotationId: number; @ManyToOne('ExecutionAnnotation', 'tagMappings') @JoinColumn({ name: 'annotationId' }) diff --git a/packages/cli/src/databases/entities/ExecutionAnnotation.ts b/packages/cli/src/databases/entities/ExecutionAnnotation.ts index 093e289e01792..91d1ff31a8d08 100644 --- a/packages/cli/src/databases/entities/ExecutionAnnotation.ts +++ b/packages/cli/src/databases/entities/ExecutionAnnotation.ts @@ -11,8 +11,7 @@ import { import { ExecutionEntity } from './ExecutionEntity'; import type { AnnotationTagEntity } from './AnnotationTagEntity'; import type { AnnotationTagMapping } from './AnnotationTagMapping'; - -export type AnnotationVote = 'up' | 'down'; +import { AnnotationVote } from 'n8n-workflow'; @Entity({ name: 'execution_annotations' }) export class ExecutionAnnotation { diff --git a/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts index 5d3135c34caff..62f3d98c4eb6b 100644 --- a/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts +++ b/packages/cli/src/databases/migrations/common/1723039529455-CreateExecutionAnnotationTables.ts @@ -13,7 +13,7 @@ export class CreateAnnotationTables1723039529455 implements ReversibleMigration column('vote').varchar(6), column('note').text, ) - .withIndexOn('executionId') + .withIndexOn('executionId', true) .withForeignKey('executionId', { tableName: 'execution_entity', columnName: 'id', diff --git a/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts new file mode 100644 index 0000000000000..fe75b03e1a155 --- /dev/null +++ b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts @@ -0,0 +1,33 @@ +import { Service } from 'typedi'; +import { DataSource, Repository } from '@n8n/typeorm'; +import { AnnotationTagMapping } from '@db/entities/AnnotationTagMapping'; +import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; +import { ApplicationError } from 'n8n-workflow'; + +@Service() +export class AnnotationTagMappingRepository extends Repository { + constructor(dataSource: DataSource) { + super(AnnotationTagMapping, dataSource.manager); + } + + async overwriteTags(executionId: string, tagIds: string[]) { + return await this.manager.transaction(async (tx) => { + await tx.upsert(ExecutionAnnotation, { execution: { id: executionId } }, ['execution']); + const annotation = await tx.findOne(ExecutionAnnotation, { + where: { execution: { id: executionId } }, + }); + + if (!annotation) { + throw new ApplicationError(`Annotation for execution ${executionId} not found`); + } + + await tx.delete(AnnotationTagMapping, { annotationId: annotation.id }); + + const tagMappings = tagIds.map((tagId) => + this.create({ annotationId: annotation.id, tagId }), + ); + + return await tx.insert(AnnotationTagMapping, tagMappings); + }); + } +} diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 8ac8de77cdef9..f70f6d2ce17c3 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -207,6 +207,7 @@ export class ExecutionRepository extends Repository { id: string, options?: { includeData: true; + includeAnnotation?: boolean; unflattenData: true; where?: FindOptionsWhere; }, @@ -215,6 +216,7 @@ export class ExecutionRepository extends Repository { id: string, options?: { includeData: true; + includeAnnotation?: boolean; unflattenData?: false | undefined; where?: FindOptionsWhere; }, @@ -223,6 +225,7 @@ export class ExecutionRepository extends Repository { id: string, options?: { includeData?: boolean; + includeAnnotation?: boolean; unflattenData?: boolean; where?: FindOptionsWhere; }, @@ -231,6 +234,7 @@ export class ExecutionRepository extends Repository { id: string, options?: { includeData?: boolean; + includeAnnotation?: boolean; unflattenData?: boolean; where?: FindOptionsWhere; }, @@ -242,7 +246,16 @@ export class ExecutionRepository extends Repository { }, }; if (options?.includeData) { - findOptions.relations = ['executionData', 'metadata']; + findOptions.relations = { executionData: true, metadata: true }; + } + + if (options?.includeAnnotation) { + findOptions.relations = { + ...findOptions.relations, + annotation: { + tags: true, + }, + }; } const execution = await this.findOne(findOptions); @@ -251,7 +264,17 @@ export class ExecutionRepository extends Repository { return undefined; } - const { executionData, metadata, ...rest } = execution; + const { executionData, metadata, annotation, ...rest } = execution; + + console.log({ annotation }); + + const sanitizedAnnotation = annotation + ? { + id: annotation.id, + vote: annotation.vote, + tags: annotation.tags.map(({ id, name }) => ({ id, name })), + } + : annotation; if (options?.includeData && options?.unflattenData) { return { @@ -259,6 +282,7 @@ export class ExecutionRepository extends Repository { data: parse(execution.executionData.data) as IRunExecutionData, workflowData: execution.executionData.workflowData, customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])), + annotation: sanitizedAnnotation, } as IExecutionResponse; } else if (options?.includeData) { return { @@ -266,6 +290,7 @@ export class ExecutionRepository extends Repository { data: execution.executionData.data, workflowData: execution.executionData.workflowData, customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])), + annotation: sanitizedAnnotation, } as IExecutionFlattedDb; } @@ -614,6 +639,7 @@ export class ExecutionRepository extends Repository { }, includeData: true, unflattenData: true, + includeAnnotation: true, }); } @@ -624,6 +650,7 @@ export class ExecutionRepository extends Repository { }, includeData: true, unflattenData: false, + includeAnnotation: true, }); } @@ -698,7 +725,8 @@ export class ExecutionRepository extends Repository { const qb = this.toQueryBuilderWithAnnotations(query); console.log(qb.getQuery()); - const rawExecutionsWithTags: ExecutionSummary[] = await qb.getRawMany(); + const rawExecutionsWithTags: Array = + await qb.getRawMany(); // FIXME: This needs refactoring const executions = rawExecutionsWithTags.reduce( @@ -706,12 +734,12 @@ export class ExecutionRepository extends Repository { const existingExecution = acc.find((e) => e.id === row.id); if (existingExecution) { if (tagId) { - existingExecution.tags.push({ id: tagId as string, name: tagName as string }); + existingExecution.tags.push({ id: tagId, name: tagName }); } } else { acc.push({ ...row, - tags: tagId ? [{ id: tagId as string, name: tagName as string }] : [], + tags: tagId ? [{ id: tagId, name: tagName }] : [], }); } return acc; @@ -863,6 +891,9 @@ export class ExecutionRepository extends Repository { // This method is used to add the annotation fields to the executions query // It uses original query builder as a subquery and adds the annotation fields to it + // FIXME: Query made with this query builder fetches duplicate executions for each tag, + // this is intended, as we are working with raw query. + // private toQueryBuilderWithAnnotations(query: ExecutionSummaries.Query) { const annotationFields = Object.keys(this.annotationFields).map( (key) => `annotation.${key} AS "annotation.${key}"`, diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 73e010fc53d2b..1f9bbf9c701bc 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -40,6 +40,7 @@ import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { License } from '@/License'; +import { AnnotationTagMappingRepository } from '@db/repositories/annotationTagMapping.repository'; export const schemaGetExecutionsQueryFilter = { $id: '/IGetExecutionsQueryFilter', @@ -86,6 +87,7 @@ export class ExecutionService { private readonly logger: Logger, private readonly queue: Queue, private readonly activeExecutions: ActiveExecutions, + private readonly annotationTagMappingRepository: AnnotationTagMappingRepository, private readonly executionRepository: ExecutionRepository, private readonly workflowRepository: WorkflowRepository, private readonly nodeTypes: NodeTypes, @@ -479,4 +481,12 @@ export class ExecutionService { return await this.executionRepository.stopDuringRun(execution); } + + public async update(executionId: string, updateData: { tags: string[] }) { + if (updateData.tags) { + await this.annotationTagMappingRepository.overwriteTags(executionId, updateData.tags); + } + + return true; + } } diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts index 7d5ffe393e57a..39eca4a6d7724 100644 --- a/packages/cli/src/executions/execution.types.ts +++ b/packages/cli/src/executions/execution.types.ts @@ -28,6 +28,11 @@ export declare namespace ExecutionRequest { }; } + type ExecutionUpdatePayload = { + id: number; + tags: string[]; + }; + type GetMany = AuthenticatedRequest<{}, {}, {}, QueryParams.GetMany> & { rangeQuery: ExecutionSummaries.RangeQuery; // parsed from query params }; @@ -39,6 +44,8 @@ export declare namespace ExecutionRequest { type Retry = AuthenticatedRequest; type Stop = AuthenticatedRequest; + + type Update = AuthenticatedRequest; } export namespace ExecutionSummaries { diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 64b6a5427429f..4b0e0e9ce56a5 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -1,6 +1,6 @@ import { ExecutionRequest } from './execution.types'; import { ExecutionService } from './execution.service'; -import { Get, Post, RestController } from '@/decorators'; +import { Get, Patch, Post, RestController } from '@/decorators'; import { EnterpriseExecutionsService } from './execution.service.ee'; import { License } from '@/License'; import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; @@ -100,4 +100,25 @@ export class ExecutionsController { return await this.executionService.delete(req, workflowIds); } + + @Patch('/:id') + async update(req: ExecutionRequest.Update) { + if (!isPositiveInteger(req.params.id)) { + throw new BadRequestError('Execution ID is not a number'); + } + + const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:read'); + + if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); + + const { tags } = req.body; + + if (tags) { + const updatedExecution = await this.executionService.update(req.params.id, { tags }); + + return updatedExecution; + } else { + throw new BadRequestError('No annotation provided'); + } + } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 5d22da977e947..4117befabe9d2 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2426,6 +2426,8 @@ export interface NodeExecutionWithMetadata extends INodeExecutionData { pairedItem: IPairedItemData | IPairedItemData[]; } +export type AnnotationVote = 'up' | 'down'; + export interface ExecutionSummary { id: string; finished?: boolean; From 7f4db8f2c6d2df3ce70548171462d63a3d1db608 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Mon, 12 Aug 2024 13:43:09 +0200 Subject: [PATCH 10/79] wip: annotation endpoints --- .../src/databases/entities/ExecutionAnnotation.ts | 2 +- .../cli/src/databases/entities/ExecutionEntity.ts | 2 +- .../repositories/annotationTagMapping.repository.ts | 13 ++++++++++--- .../databases/repositories/execution.repository.ts | 5 ++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/databases/entities/ExecutionAnnotation.ts b/packages/cli/src/databases/entities/ExecutionAnnotation.ts index 91d1ff31a8d08..baede2d970dcd 100644 --- a/packages/cli/src/databases/entities/ExecutionAnnotation.ts +++ b/packages/cli/src/databases/entities/ExecutionAnnotation.ts @@ -8,7 +8,7 @@ import { PrimaryGeneratedColumn, RelationId, } from '@n8n/typeorm'; -import { ExecutionEntity } from './ExecutionEntity'; +import type { ExecutionEntity } from './ExecutionEntity'; import type { AnnotationTagEntity } from './AnnotationTagEntity'; import type { AnnotationTagMapping } from './AnnotationTagMapping'; import { AnnotationVote } from 'n8n-workflow'; diff --git a/packages/cli/src/databases/entities/ExecutionEntity.ts b/packages/cli/src/databases/entities/ExecutionEntity.ts index 9e480a3fc7cf5..82c9d28c5cc30 100644 --- a/packages/cli/src/databases/entities/ExecutionEntity.ts +++ b/packages/cli/src/databases/entities/ExecutionEntity.ts @@ -16,7 +16,7 @@ import { idStringifier } from '../utils/transformers'; import type { ExecutionData } from './ExecutionData'; import type { ExecutionMetadata } from './ExecutionMetadata'; import { WorkflowEntity } from './WorkflowEntity'; -import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; +import type { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; @Entity() @Index(['workflowId', 'id']) diff --git a/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts index fe75b03e1a155..65c9c29c6cf26 100644 --- a/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts +++ b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts @@ -23,9 +23,16 @@ export class AnnotationTagMappingRepository extends Repository - this.create({ annotationId: annotation.id, tagId }), - ); + const tagMappings = tagIds.map((tagId) => { + this.create({ annotationId: annotation.id, tagId }); + + // FIXME: for some reason tx.insert below throws type error if we use this.create result directly, + // so we have to create plain objects + return { + annotationId: annotation.id, + tagId, + }; + }); return await tx.insert(AnnotationTagMapping, tagMappings); }); diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index f70f6d2ce17c3..b787bbfcaedf7 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -1,4 +1,5 @@ import { Service } from 'typedi'; +import pick from 'lodash/pick'; import { Brackets, DataSource, @@ -266,13 +267,11 @@ export class ExecutionRepository extends Repository { const { executionData, metadata, annotation, ...rest } = execution; - console.log({ annotation }); - const sanitizedAnnotation = annotation ? { id: annotation.id, vote: annotation.vote, - tags: annotation.tags.map(({ id, name }) => ({ id, name })), + tags: annotation.tags?.map((tag) => pick(tag, ['id', 'name'])) ?? [], } : annotation; From 01b17d2e14e7091f30576980ed9bff70e5b1c2fd Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Tue, 13 Aug 2024 16:51:58 +0200 Subject: [PATCH 11/79] wip: execution annotation sidebar --- .../repositories/execution.repository.ts | 39 ++- packages/editor-ui/src/Interface.ts | 1 + packages/editor-ui/src/api/annotationTags.ts | 22 ++ packages/editor-ui/src/api/tags.ts | 58 +++-- packages/editor-ui/src/components/Modals.vue | 5 + .../editor-ui/src/components/TagsDropdown.vue | 6 +- .../components/TagsManager/TagsManager.vue | 18 +- .../executions/ExecutionsFilter.vue | 21 ++ .../WorkflowExecutionAnnotationSidebar.vue | 227 +++++++++++++++++ .../workflow/WorkflowExecutionsCard.vue | 36 ++- .../workflow/WorkflowExecutionsList.vue | 1 + .../src/composables/useExecutionHelpers.ts | 2 +- packages/editor-ui/src/constants.ts | 2 + .../src/plugins/i18n/locales/en.json | 4 + packages/editor-ui/src/stores/tags.store.ts | 234 ++++++++++-------- packages/editor-ui/src/stores/ui.store.ts | 2 + 16 files changed, 538 insertions(+), 140 deletions(-) create mode 100644 packages/editor-ui/src/api/annotationTags.ts create mode 100644 packages/editor-ui/src/components/executions/workflow/WorkflowExecutionAnnotationSidebar.vue diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index b787bbfcaedf7..41e96f3ec2bad 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -23,6 +23,7 @@ import type { import { parse, stringify } from 'flatted'; import { GlobalConfig } from '@n8n/config'; import { + AnnotationVote, ApplicationError, type ExecutionStatus, type ExecutionSummary, @@ -722,28 +723,50 @@ export class ExecutionRepository extends Repository { } const qb = this.toQueryBuilderWithAnnotations(query); - console.log(qb.getQuery()); - const rawExecutionsWithTags: Array = - await qb.getRawMany(); + const rawExecutionsWithTags: Array< + ExecutionSummary & { + 'annotation.id': number; + 'annotation.vote': AnnotationVote; + 'annotation.tags.id': string; + 'annotation.tags.name': string; + } + > = await qb.getRawMany(); // FIXME: This needs refactoring const executions = rawExecutionsWithTags.reduce( - (acc, { tagId, tagName, ...row }) => { + ( + acc, + { + 'annotation.id': _, + 'annotation.vote': vote, + 'annotation.tags.id': tagId, + 'annotation.tags.name': tagName, + ...row + }, + ) => { const existingExecution = acc.find((e) => e.id === row.id); + if (existingExecution) { if (tagId) { - existingExecution.tags.push({ id: tagId, name: tagName }); + existingExecution.annotation = existingExecution.annotation ?? { + vote, + tags: [] as Array<{ id: string; name: string }>, + }; + existingExecution.annotation.tags.push({ id: tagId, name: tagName }); } } else { acc.push({ ...row, - tags: tagId ? [{ id: tagId, name: tagName }] : [], + annotation: { + vote, + tags: tagId ? [{ id: tagId, name: tagName }] : [], + }, }); } return acc; }, - [] as Array }>, + [] as ExecutionSummary[], ); return executions.map((execution) => this.toSummary(execution)); @@ -909,7 +932,7 @@ export class ExecutionRepository extends Repository { return this.manager .createQueryBuilder() - .select(['e.*', 'ate.id AS "tagId"', 'ate.name AS "tagName"']) + .select(['e.*', 'ate.id AS "annotation.tags.id"', 'ate.name AS "annotation.tags.name"']) .from(`(${subQuery.getQuery()})`, 'e') .setParameters(subQuery.getParameters()) .leftJoin(AnnotationTagMapping, 'atm', 'atm.annotationId = e."annotation.id"') diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 8c93c847ba540..d46aa0326193a 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1583,6 +1583,7 @@ export type ExecutionFilterType = { startDate: string | Date; endDate: string | Date; tags: string[]; + annotationTags: string[]; metadata: ExecutionFilterMetadata[]; }; diff --git a/packages/editor-ui/src/api/annotationTags.ts b/packages/editor-ui/src/api/annotationTags.ts new file mode 100644 index 0000000000000..283ea22254a39 --- /dev/null +++ b/packages/editor-ui/src/api/annotationTags.ts @@ -0,0 +1,22 @@ +import type { IRestApiContext, ITag } from '@/Interface'; +import { makeRestApiRequest } from '@/utils/apiUtils'; + +export async function getTags(context: IRestApiContext, withUsageCount = false): Promise { + return await makeRestApiRequest(context, 'GET', '/annotation-tags', { withUsageCount }); +} + +export async function createTag(context: IRestApiContext, params: { name: string }): Promise { + return await makeRestApiRequest(context, 'POST', '/annotation-tags', params); +} + +export async function updateTag( + context: IRestApiContext, + id: string, + params: { name: string }, +): Promise { + return await makeRestApiRequest(context, 'PATCH', `/annotation-tags/${id}`, params); +} + +export async function deleteTag(context: IRestApiContext, id: string): Promise { + return await makeRestApiRequest(context, 'DELETE', `/annotation-tags/${id}`); +} diff --git a/packages/editor-ui/src/api/tags.ts b/packages/editor-ui/src/api/tags.ts index 7442a40ef16cf..2c11b997e528c 100644 --- a/packages/editor-ui/src/api/tags.ts +++ b/packages/editor-ui/src/api/tags.ts @@ -1,22 +1,52 @@ import type { IRestApiContext, ITag } from '@/Interface'; import { makeRestApiRequest } from '@/utils/apiUtils'; -export async function getTags(context: IRestApiContext, withUsageCount = false): Promise { - return await makeRestApiRequest(context, 'GET', '/tags', { withUsageCount }); -} +// export async function getTags(context: IRestApiContext, withUsageCount = false): Promise { +// return await makeRestApiRequest(context, 'GET', '/tags', { withUsageCount }); +// } +// +// export async function createTag(context: IRestApiContext, params: { name: string }): Promise { +// return await makeRestApiRequest(context, 'POST', '/tags', params); +// } +// +// export async function updateTag( +// context: IRestApiContext, +// id: string, +// params: { name: string }, +// ): Promise { +// return await makeRestApiRequest(context, 'PATCH', `/tags/${id}`, params); +// } +// +// export async function deleteTag(context: IRestApiContext, id: string): Promise { +// return await makeRestApiRequest(context, 'DELETE', `/tags/${id}`); +// } -export async function createTag(context: IRestApiContext, params: { name: string }): Promise { - return await makeRestApiRequest(context, 'POST', '/tags', params); -} +type TagsApiEndpoint = '/tags' | '/annotation-tags'; -export async function updateTag( - context: IRestApiContext, - id: string, - params: { name: string }, -): Promise { - return await makeRestApiRequest(context, 'PATCH', `/tags/${id}`, params); +export interface ITagsApi { + getTags: (context: IRestApiContext, withUsageCount?: boolean) => Promise; + createTag: (context: IRestApiContext, params: { name: string }) => Promise; + updateTag: (context: IRestApiContext, id: string, params: { name: string }) => Promise; + deleteTag: (context: IRestApiContext, id: string) => Promise; } -export async function deleteTag(context: IRestApiContext, id: string): Promise { - return await makeRestApiRequest(context, 'DELETE', `/tags/${id}`); +export function createTagsApi(endpoint: TagsApiEndpoint): ITagsApi { + return { + getTags: async (context: IRestApiContext, withUsageCount = false): Promise => { + return await makeRestApiRequest(context, 'GET', endpoint, { withUsageCount }); + }, + createTag: async (context: IRestApiContext, params: { name: string }): Promise => { + return await makeRestApiRequest(context, 'POST', endpoint, params); + }, + updateTag: async ( + context: IRestApiContext, + id: string, + params: { name: string }, + ): Promise => { + return await makeRestApiRequest(context, 'PATCH', `${endpoint}/${id}`, params); + }, + deleteTag: async (context: IRestApiContext, id: string): Promise => { + return await makeRestApiRequest(context, 'DELETE', `${endpoint}/${id}`); + }, + }; } diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 4e34daf40db9a..68df78e4f7aa7 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -13,6 +13,7 @@ import { INVITE_USER_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, + ANNOTATION_TAGS_MANAGER_MODAL_KEY, NPS_SURVEY_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY, @@ -104,6 +105,10 @@ import ProjectMoveResourceConfirmModal from '@/components/Projects/ProjectMoveRe + + + + diff --git a/packages/editor-ui/src/components/TagsDropdown.vue b/packages/editor-ui/src/components/TagsDropdown.vue index 0a5ac3b4933a3..71ad6215e26ee 100644 --- a/packages/editor-ui/src/components/TagsDropdown.vue +++ b/packages/editor-ui/src/components/TagsDropdown.vue @@ -85,6 +85,10 @@ const CREATE_KEY = '__create'; export default defineComponent({ name: 'TagsDropdown', props: { + tagsStore: { + type: Function, + default: useTagsStore, + }, placeholder: {}, modelValue: { type: Array as PropType, @@ -107,7 +111,7 @@ export default defineComponent({ setup(props, { emit }) { const i18n = useI18n(); const { showError } = useToast(); - const tagsStore = useTagsStore(); + const tagsStore = props.tagsStore(); const uiStore = useUIStore(); const { isLoading } = storeToRefs(tagsStore); diff --git a/packages/editor-ui/src/components/TagsManager/TagsManager.vue b/packages/editor-ui/src/components/TagsManager/TagsManager.vue index 02bfee1dd2b86..d4b9d5a50b94b 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsManager.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsManager.vue @@ -29,7 +29,7 @@ + + + + diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue index 60f9ec61eec43..d888cad832f94 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue @@ -56,7 +56,20 @@ {{ $locale.baseText('executionDetails.retry') }} #{{ execution.retryOf }}
- +
+
+ + +
+ +
{{ $locale.baseText('executionsList.test') }} -
+ diff --git a/packages/editor-ui/src/composables/useExecutionHelpers.ts b/packages/editor-ui/src/composables/useExecutionHelpers.ts index 71974bfb5cf20..a65182c475272 100644 --- a/packages/editor-ui/src/composables/useExecutionHelpers.ts +++ b/packages/editor-ui/src/composables/useExecutionHelpers.ts @@ -21,7 +21,7 @@ export function useExecutionHelpers() { label: 'Status unknown', runningTime: '', showTimestamp: true, - tags: execution.tags, + tags: execution.annotation?.tags ?? [], }; if (execution.status === 'new') { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index acf94fcc3c0dd..8184aaae3483d 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -44,6 +44,7 @@ export const DELETE_USER_MODAL_KEY = 'deleteUser'; export const INVITE_USER_MODAL_KEY = 'inviteUser'; export const DUPLICATE_MODAL_KEY = 'duplicate'; export const TAGS_MANAGER_MODAL_KEY = 'tagsManager'; +export const ANNOTATION_TAGS_MANAGER_MODAL_KEY = 'annotationTagsManager'; export const VERSIONS_MODAL_KEY = 'versions'; export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings'; export const WORKFLOW_LM_CHAT_MODAL_KEY = 'lmChat'; @@ -620,6 +621,7 @@ export const enum STORES { NODE_TYPES = 'nodeTypes', CREDENTIALS = 'credentials', TAGS = 'tags', + ANNOTATION_TAGS = 'annotationTags', VERSIONS = 'versions', NODE_CREATOR = 'nodeCreator', WEBHOOKS = 'webhooks', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e4b312f77ff4b..0b660dab7dcf4 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -28,6 +28,8 @@ "clientSecret": "Client Secret" } }, + "generic.annotations": "Annotations", + "generic.annotationData": "Annotation data", "generic.any": "Any", "generic.cancel": "Cancel", "generic.close": "Close", @@ -51,6 +53,7 @@ "generic.beta": "beta", "generic.yes": "Yes", "generic.no": "No", + "generic.rating": "Rating", "generic.retry": "Retry", "generic.error": "Something went wrong", "generic.settings": "Settings", @@ -748,6 +751,7 @@ "executionView.onPaste.title": "Cannot paste here", "executionView.onPaste.message": "This view is read-only. Switch to Workflow tab to be able to edit the current workflow", "executionView.notFound.message": "Execution with id '{executionId}' could not be found!", + "executionsFilter.annotation.tags": "Annotation tags", "executionsFilter.selectStatus": "Select Status", "executionsFilter.selectWorkflow": "Select Workflow", "executionsFilter.start": "Execution start", diff --git a/packages/editor-ui/src/stores/tags.store.ts b/packages/editor-ui/src/stores/tags.store.ts index 4dab82a8cb711..fade74a39c03e 100644 --- a/packages/editor-ui/src/stores/tags.store.ts +++ b/packages/editor-ui/src/stores/tags.store.ts @@ -1,4 +1,4 @@ -import * as tagsApi from '@/api/tags'; +import { createTagsApi } from '@/api/tags'; import { STORES } from '@/constants'; import type { ITag } from '@/Interface'; import { defineStore } from 'pinia'; @@ -6,109 +6,129 @@ import { useRootStore } from './root.store'; import { computed, ref } from 'vue'; import { useWorkflowsStore } from './workflows.store'; -export const useTagsStore = defineStore(STORES.TAGS, () => { - const tagsById = ref>({}); - const loading = ref(false); - const fetchedAll = ref(false); - const fetchedUsageCount = ref(false); - - const rootStore = useRootStore(); - const workflowsStore = useWorkflowsStore(); - - // Computed - - const allTags = computed(() => { - return Object.values(tagsById.value).sort((a, b) => a.name.localeCompare(b.name)); - }); - - const isLoading = computed(() => loading.value); - - const hasTags = computed(() => Object.keys(tagsById.value).length > 0); - - // Methods - - const setAllTags = (loadedTags: ITag[]) => { - tagsById.value = loadedTags.reduce((accu: { [id: string]: ITag }, tag: ITag) => { - accu[tag.id] = tag; - - return accu; - }, {}); - fetchedAll.value = true; - }; - - const upsertTags = (toUpsertTags: ITag[]) => { - toUpsertTags.forEach((toUpsertTag) => { - const tagId = toUpsertTag.id; - const currentTag = tagsById.value[tagId]; - if (currentTag) { - const newTag = { - ...currentTag, - ...toUpsertTag, - }; - tagsById.value = { - ...tagsById.value, - [tagId]: newTag, - }; - } else { - tagsById.value = { - ...tagsById.value, - [tagId]: toUpsertTag, - }; - } - }); - }; - - const deleteTag = (id: string) => { - const { [id]: deleted, ...rest } = tagsById.value; - tagsById.value = rest; - }; - - const fetchAll = async (params?: { force?: boolean; withUsageCount?: boolean }) => { - const { force = false, withUsageCount = false } = params || {}; - if (!force && fetchedAll.value && fetchedUsageCount.value === withUsageCount) { - return Object.values(tagsById.value); - } - - loading.value = true; - const retrievedTags = await tagsApi.getTags(rootStore.restApiContext, Boolean(withUsageCount)); - setAllTags(retrievedTags); - loading.value = false; - return retrievedTags; - }; - - const create = async (name: string) => { - const createdTag = await tagsApi.createTag(rootStore.restApiContext, { name }); - upsertTags([createdTag]); - return createdTag; - }; - - const rename = async ({ id, name }: { id: string; name: string }) => { - const updatedTag = await tagsApi.updateTag(rootStore.restApiContext, id, { name }); - upsertTags([updatedTag]); - return updatedTag; - }; - - const deleteTagById = async (id: string) => { - const deleted = await tagsApi.deleteTag(rootStore.restApiContext, id); - - if (deleted) { - deleteTag(id); - workflowsStore.removeWorkflowTagId(id); - } - - return deleted; - }; - - return { - allTags, - isLoading, - hasTags, - tagsById, - fetchAll, - create, - rename, - deleteTagById, - upsertTags, - deleteTag, - }; -}); +const apiMapping = { + [STORES.TAGS]: createTagsApi('/tags'), + [STORES.ANNOTATION_TAGS]: createTagsApi('/annotation-tags'), +}; + +const createTagsStore = (id: STORES.TAGS | STORES.ANNOTATION_TAGS) => { + const tagsApi = apiMapping[id]; + + return defineStore( + id, + () => { + const tagsById = ref>({}); + const loading = ref(false); + const fetchedAll = ref(false); + const fetchedUsageCount = ref(false); + + const rootStore = useRootStore(); + const workflowsStore = useWorkflowsStore(); + + // Computed + + const allTags = computed(() => { + return Object.values(tagsById.value).sort((a, b) => a.name.localeCompare(b.name)); + }); + + const isLoading = computed(() => loading.value); + + const hasTags = computed(() => Object.keys(tagsById.value).length > 0); + + // Methods + + const setAllTags = (loadedTags: ITag[]) => { + tagsById.value = loadedTags.reduce((accu: { [id: string]: ITag }, tag: ITag) => { + accu[tag.id] = tag; + + return accu; + }, {}); + fetchedAll.value = true; + }; + + const upsertTags = (toUpsertTags: ITag[]) => { + toUpsertTags.forEach((toUpsertTag) => { + const tagId = toUpsertTag.id; + const currentTag = tagsById.value[tagId]; + if (currentTag) { + const newTag = { + ...currentTag, + ...toUpsertTag, + }; + tagsById.value = { + ...tagsById.value, + [tagId]: newTag, + }; + } else { + tagsById.value = { + ...tagsById.value, + [tagId]: toUpsertTag, + }; + } + }); + }; + + const deleteTag = (id: string) => { + const { [id]: deleted, ...rest } = tagsById.value; + tagsById.value = rest; + }; + + const fetchAll = async (params?: { force?: boolean; withUsageCount?: boolean }) => { + const { force = false, withUsageCount = false } = params || {}; + if (!force && fetchedAll.value && fetchedUsageCount.value === withUsageCount) { + return Object.values(tagsById.value); + } + + loading.value = true; + const retrievedTags = await tagsApi.getTags( + rootStore.restApiContext, + Boolean(withUsageCount), + ); + setAllTags(retrievedTags); + loading.value = false; + return retrievedTags; + }; + + const create = async (name: string) => { + const createdTag = await tagsApi.createTag(rootStore.restApiContext, { name }); + upsertTags([createdTag]); + return createdTag; + }; + + const rename = async ({ id, name }: { id: string; name: string }) => { + const updatedTag = await tagsApi.updateTag(rootStore.restApiContext, id, { name }); + upsertTags([updatedTag]); + return updatedTag; + }; + + const deleteTagById = async (id: string) => { + const deleted = await tagsApi.deleteTag(rootStore.restApiContext, id); + + if (deleted) { + deleteTag(id); + workflowsStore.removeWorkflowTagId(id); + } + + return deleted; + }; + + return { + allTags, + isLoading, + hasTags, + tagsById, + fetchAll, + create, + rename, + deleteTagById, + upsertTags, + deleteTag, + }; + }, + {}, + ); +}; + +export const useTagsStore = createTagsStore(STORES.TAGS); + +export const useAnnotationTagsStore = createTagsStore(STORES.ANNOTATION_TAGS); diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index e8571181587a2..ddddd4976a6c8 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -19,6 +19,7 @@ import { PERSONALIZATION_MODAL_KEY, STORES, TAGS_MANAGER_MODAL_KEY, + ANNOTATION_TAGS_MANAGER_MODAL_KEY, NPS_SURVEY_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS, @@ -106,6 +107,7 @@ export const useUIStore = defineStore(STORES.UI, () => { PERSONALIZATION_MODAL_KEY, INVITE_USER_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, + ANNOTATION_TAGS_MANAGER_MODAL_KEY, NPS_SURVEY_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_LM_CHAT_MODAL_KEY, From f9264135b9adf444a4fbf98c57e3c9f3727e83ed Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Wed, 14 Aug 2024 10:46:33 +0200 Subject: [PATCH 12/79] wip: save execution rating annotation --- .../repositories/annotationTagMapping.repository.ts | 1 - .../repositories/executionAnnotation.repository.ts | 10 ++++++++++ packages/cli/src/executions/execution.service.ts | 11 ++++++++++- packages/cli/src/executions/execution.types.ts | 8 +++++++- packages/cli/src/executions/executions.controller.ts | 8 +++----- 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 packages/cli/src/databases/repositories/executionAnnotation.repository.ts diff --git a/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts index 65c9c29c6cf26..fd3ebec0e6fe3 100644 --- a/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts +++ b/packages/cli/src/databases/repositories/annotationTagMapping.repository.ts @@ -12,7 +12,6 @@ export class AnnotationTagMappingRepository extends Repository { - await tx.upsert(ExecutionAnnotation, { execution: { id: executionId } }, ['execution']); const annotation = await tx.findOne(ExecutionAnnotation, { where: { execution: { id: executionId } }, }); diff --git a/packages/cli/src/databases/repositories/executionAnnotation.repository.ts b/packages/cli/src/databases/repositories/executionAnnotation.repository.ts new file mode 100644 index 0000000000000..23827fddc1f1d --- /dev/null +++ b/packages/cli/src/databases/repositories/executionAnnotation.repository.ts @@ -0,0 +1,10 @@ +import { Service } from 'typedi'; +import { DataSource, Repository } from '@n8n/typeorm'; +import { ExecutionAnnotation } from '@db/entities/ExecutionAnnotation'; + +@Service() +export class ExecutionAnnotationRepository extends Repository { + constructor(dataSource: DataSource) { + super(ExecutionAnnotation, dataSource.manager); + } +} diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 1f9bbf9c701bc..0c48969e5b795 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -8,6 +8,7 @@ import type { IRunExecutionData, WorkflowExecuteMode, ExecutionStatus, + AnnotationVote, } from 'n8n-workflow'; import { ApplicationError, @@ -41,6 +42,7 @@ import { ConcurrencyControlService } from '@/concurrency/concurrency-control.ser import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { License } from '@/License'; import { AnnotationTagMappingRepository } from '@db/repositories/annotationTagMapping.repository'; +import { ExecutionAnnotationRepository } from '@db/repositories/executionAnnotation.repository'; export const schemaGetExecutionsQueryFilter = { $id: '/IGetExecutionsQueryFilter', @@ -87,6 +89,7 @@ export class ExecutionService { private readonly logger: Logger, private readonly queue: Queue, private readonly activeExecutions: ActiveExecutions, + private readonly executionAnnotationRepository: ExecutionAnnotationRepository, private readonly annotationTagMappingRepository: AnnotationTagMappingRepository, private readonly executionRepository: ExecutionRepository, private readonly workflowRepository: WorkflowRepository, @@ -482,7 +485,13 @@ export class ExecutionService { return await this.executionRepository.stopDuringRun(execution); } - public async update(executionId: string, updateData: { tags: string[] }) { + public async annotate(executionId: string, updateData: { tags: string[]; vote: AnnotationVote }) { + // FIXME: wrap in transaction + await this.executionAnnotationRepository.upsert( + { execution: { id: executionId }, vote: updateData.vote }, + ['execution'], + ); + if (updateData.tags) { await this.annotationTagMappingRepository.overwriteTags(executionId, updateData.tags); } diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts index 39eca4a6d7724..cf9cdb7f56563 100644 --- a/packages/cli/src/executions/execution.types.ts +++ b/packages/cli/src/executions/execution.types.ts @@ -1,6 +1,11 @@ import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity'; import type { AuthenticatedRequest } from '@/requests'; -import type { ExecutionStatus, IDataObject, WorkflowExecuteMode } from 'n8n-workflow'; +import type { + AnnotationVote, + ExecutionStatus, + IDataObject, + WorkflowExecuteMode, +} from 'n8n-workflow'; export declare namespace ExecutionRequest { namespace QueryParams { @@ -31,6 +36,7 @@ export declare namespace ExecutionRequest { type ExecutionUpdatePayload = { id: number; tags: string[]; + vote: AnnotationVote; }; type GetMany = AuthenticatedRequest<{}, {}, {}, QueryParams.GetMany> & { diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 4b0e0e9ce56a5..2d51aa963237c 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -111,12 +111,10 @@ export class ExecutionsController { if (workflowIds.length === 0) throw new NotFoundError('Execution not found'); - const { tags } = req.body; + const { tags, vote } = req.body; - if (tags) { - const updatedExecution = await this.executionService.update(req.params.id, { tags }); - - return updatedExecution; + if (tags || vote) { + return await this.executionService.annotate(req.params.id, { tags, vote }); } else { throw new BadRequestError('No annotation provided'); } From ed653da4e4a1c8907c74866c39cba635d785a280 Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Wed, 14 Aug 2024 14:59:47 +0200 Subject: [PATCH 13/79] wip: Refactor TagsDropdown and TagsManager --- .../src/components/AnnotationTagsDropdown.vue | 87 +++++ .../components/MainHeader/WorkflowDetails.vue | 2 +- packages/editor-ui/src/components/Modals.vue | 3 +- .../editor-ui/src/components/TagsDropdown.vue | 6 +- .../src/components/TagsDropdownPure.vue | 369 ++++++++++++++++++ .../components/TagsManager/TagsManager.vue | 16 +- .../TagsManager/TagsManagerPure.vue | 169 ++++++++ .../TagsManager/WorkflowTagsManager.vue | 106 +++++ .../src/components/WorkflowTagsDropdown.vue | 87 +++++ .../src/plugins/i18n/locales/en.json | 2 + .../editor-ui/src/views/WorkflowsView.vue | 2 +- 11 files changed, 830 insertions(+), 19 deletions(-) create mode 100644 packages/editor-ui/src/components/AnnotationTagsDropdown.vue create mode 100644 packages/editor-ui/src/components/TagsDropdownPure.vue create mode 100644 packages/editor-ui/src/components/TagsManager/TagsManagerPure.vue create mode 100644 packages/editor-ui/src/components/TagsManager/WorkflowTagsManager.vue create mode 100644 packages/editor-ui/src/components/WorkflowTagsDropdown.vue diff --git a/packages/editor-ui/src/components/AnnotationTagsDropdown.vue b/packages/editor-ui/src/components/AnnotationTagsDropdown.vue new file mode 100644 index 0000000000000..37c2ced970fe4 --- /dev/null +++ b/packages/editor-ui/src/components/AnnotationTagsDropdown.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index fb103720206e5..8d6503872b0de 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -635,7 +635,7 @@ function showCreateWorkflowSuccessToast(id?: string) { - diff --git a/packages/editor-ui/src/components/TagsManager/WorkflowTagsManager.vue b/packages/editor-ui/src/components/TagsManager/WorkflowTagsManager.vue new file mode 100644 index 0000000000000..293a3a62ec98f --- /dev/null +++ b/packages/editor-ui/src/components/TagsManager/WorkflowTagsManager.vue @@ -0,0 +1,106 @@ + + + diff --git a/packages/editor-ui/src/components/WorkflowTagsDropdown.vue b/packages/editor-ui/src/components/WorkflowTagsDropdown.vue new file mode 100644 index 0000000000000..7bc43c5efc761 --- /dev/null +++ b/packages/editor-ui/src/components/WorkflowTagsDropdown.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 0b660dab7dcf4..3100ca1c8c627 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1906,6 +1906,8 @@ "tagsManager.couldNotDeleteTag": "Could not delete tag", "tagsManager.done": "Done", "tagsManager.manageTags": "Manage tags", + "tagsManager.showError.onFetch.title": "Could not fetch tags", + "tagsManager.showError.onFetch.message": "A problem occurred when trying to fetch tags", "tagsManager.showError.onCreate.message": "A problem occurred when trying to create the tag '{escapedName}'", "tagsManager.showError.onCreate.title": "Could not create tag", "tagsManager.showError.onDelete.message": "A problem occurred when trying to delete the tag '{escapedName}'", diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue index 252b64dda1c2d..6d2e369990d4a 100644 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ b/packages/editor-ui/src/views/WorkflowsView.vue @@ -111,7 +111,7 @@ color="text-base" class="mb-3xs" /> - Date: Wed, 14 Aug 2024 15:00:08 +0200 Subject: [PATCH 14/79] wip: annotation tags filter --- .../repositories/execution.repository.ts | 8 +-- .../cli/src/executions/execution.service.ts | 1 + .../cli/src/executions/execution.types.ts | 2 +- .../executions/ExecutionsFilter.vue | 51 ++++++++----------- .../editor-ui/src/utils/executionUtils.ts | 5 ++ 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 41e96f3ec2bad..1ef945a0e3b4e 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -724,6 +724,7 @@ export class ExecutionRepository extends Repository { const qb = this.toQueryBuilderWithAnnotations(query); + // FIXME: This needs refactoring const rawExecutionsWithTags: Array< ExecutionSummary & { 'annotation.id': number; @@ -733,7 +734,6 @@ export class ExecutionRepository extends Repository { } > = await qb.getRawMany(); - // FIXME: This needs refactoring const executions = rawExecutionsWithTags.reduce( ( acc, @@ -843,7 +843,7 @@ export class ExecutionRepository extends Repository { startedBefore, startedAfter, metadata, - tags, + annotationTags, } = query; const fields = Object.keys(this.summaryFields) @@ -892,12 +892,12 @@ export class ExecutionRepository extends Repository { qb.setParameter('value', value); } - if (tags?.length) { + if (annotationTags?.length) { // If there is a filter by one or multiple tags, we need to join the annotations table qb.innerJoin('execution.annotation', 'annotation'); // Add an inner join for each tag - for (const tag of tags) { + for (const tag of annotationTags) { qb.innerJoin( AnnotationTagMapping, `atm_${tag}`, diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 0c48969e5b795..3bdd030f9130d 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -63,6 +63,7 @@ export const schemaGetExecutionsQueryFilter = { startedAfter: { type: 'date-time' }, startedBefore: { type: 'date-time' }, tags: { type: 'array', items: { type: 'string' } }, + annotationTags: { type: 'array', items: { type: 'string' } }, }, $defs: { metadata: { diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts index cf9cdb7f56563..551d30a45aef5 100644 --- a/packages/cli/src/executions/execution.types.ts +++ b/packages/cli/src/executions/execution.types.ts @@ -76,7 +76,7 @@ export namespace ExecutionSummaries { metadata: Array<{ key: string; value: string }>; startedAfter: string; startedBefore: string; - tags: string[]; // tag IDs + annotationTags: string[]; // tag IDs }>; type AccessFields = { diff --git a/packages/editor-ui/src/components/executions/ExecutionsFilter.vue b/packages/editor-ui/src/components/executions/ExecutionsFilter.vue index 2b05dfdaaea04..b3a99f4e2530f 100644 --- a/packages/editor-ui/src/components/executions/ExecutionsFilter.vue +++ b/packages/editor-ui/src/components/executions/ExecutionsFilter.vue @@ -7,7 +7,6 @@ import type { IWorkflowDb, } from '@/Interface'; import { i18n as locale } from '@/plugins/i18n'; -import TagsDropdown from '@/components/TagsDropdown.vue'; import { getObjectKeys, isEmpty } from '@/utils/typesUtils'; import { EnterpriseEditionFeature } from '@/constants'; import { useSettingsStore } from '@/stores/settings.store'; @@ -15,7 +14,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useTelemetry } from '@/composables/useTelemetry'; import type { Placement } from '@floating-ui/core'; import { useDebounce } from '@/composables/useDebounce'; -import { useAnnotationTagsStore } from '@/stores/tags.store'; +import AnnotationTagsDropdown from '@/components/AnnotationTagsDropdown.vue'; export type ExecutionFilterProps = { workflows?: Array; @@ -93,26 +92,17 @@ const statuses = computed(() => [ ]); const countSelectedFilterProps = computed(() => { - let count = 0; - if (filter.status !== 'all') { - count++; - } - if (filter.workflowId !== 'all' && props.workflows.length) { - count++; - } - if (!isEmpty(filter.tags)) { - count++; - } - if (!isEmpty(filter.metadata)) { - count++; - } - if (!!filter.startDate) { - count++; - } - if (!!filter.endDate) { - count++; - } - return count; + const nonDefaultFilters = [ + filter.status !== 'all', + filter.workflowId !== 'all' && props.workflows.length, + !isEmpty(filter.tags), + !isEmpty(filter.annotationTags), + !isEmpty(filter.metadata), + !!filter.startDate, + !!filter.endDate, + ].filter(Boolean); + + return nonDefaultFilters.length; }); // vModel.metadata is a text input and needs a debounced emit to avoid too many requests @@ -136,13 +126,13 @@ const onFilterMetaChange = (index: number, prop: keyof ExecutionFilterMetadata, // Can't use v-model on TagsDropdown component and thus vModel.tags is useless // We just emit the updated filter -const onTagsChange = (tags: string[]) => { - filter.tags = tags; +const onTagsChange = () => { + // filter.tags = tags; emit('filterChanged', filter); }; -const onAnnotationTagsChange = (tags: string[]) => { - filter.annotationTags = tags; +const onAnnotationTagsChange = () => { + // filter.annotationTags = tags; emit('filterChanged', filter); }; @@ -201,10 +191,10 @@ onBeforeMount(() => {
- { - diff --git a/packages/editor-ui/src/utils/executionUtils.ts b/packages/editor-ui/src/utils/executionUtils.ts index db75c5687a540..6596c5d00cd79 100644 --- a/packages/editor-ui/src/utils/executionUtils.ts +++ b/packages/editor-ui/src/utils/executionUtils.ts @@ -9,6 +9,7 @@ export function getDefaultExecutionFilters(): ExecutionFilterType { startDate: '', endDate: '', tags: [], + annotationTags: [], metadata: [], }; } @@ -25,6 +26,10 @@ export const executionFilterToQueryFilter = ( queryFilter.tags = filter.tags; } + if (!isEmpty(filter.annotationTags)) { + queryFilter.annotationTags = filter.annotationTags; + } + if (!isEmpty(filter.metadata)) { queryFilter.metadata = filter.metadata; } From 4a17f42c2c5268b97f33ff619bd977a149479eec Mon Sep 17 00:00:00 2001 From: Eugene Molodkin Date: Wed, 14 Aug 2024 17:53:06 +0200 Subject: [PATCH 15/79] wip: Annotation tags manager modal --- .../databases/entities/ExecutionAnnotation.ts | 2 +- .../src/components/AnnotationTagsDropdown.vue | 2 +- .../components/DuplicateWorkflowDialog.vue | 6 +- .../components/MainHeader/WorkflowDetails.vue | 2 +- packages/editor-ui/src/components/Modals.vue | 6 +- .../editor-ui/src/components/TagsDropdown.vue | 2 +- .../src/components/TagsDropdownPure.vue | 8 +- .../TagsManager/AnnotationTagsManager.vue | 110 ++++++++++++++++++ .../TagsManager/TagsManagerPure.vue | 16 ++- .../TagsManager/TagsView/TagsView.vue | 6 +- .../TagsManager/WorkflowTagsManager.vue | 2 + .../src/components/WorkflowTagsDropdown.vue | 1 + .../WorkflowExecutionAnnotationSidebar.vue | 4 +- .../src/plugins/i18n/locales/en.json | 2 + .../editor-ui/src/views/WorkflowsView.vue | 4 +- 15 files changed, 150 insertions(+), 23 deletions(-) create mode 100644 packages/editor-ui/src/components/TagsManager/AnnotationTagsManager.vue diff --git a/packages/cli/src/databases/entities/ExecutionAnnotation.ts b/packages/cli/src/databases/entities/ExecutionAnnotation.ts index baede2d970dcd..91d1ff31a8d08 100644 --- a/packages/cli/src/databases/entities/ExecutionAnnotation.ts +++ b/packages/cli/src/databases/entities/ExecutionAnnotation.ts @@ -8,7 +8,7 @@ import { PrimaryGeneratedColumn, RelationId, } from '@n8n/typeorm'; -import type { ExecutionEntity } from './ExecutionEntity'; +import { ExecutionEntity } from './ExecutionEntity'; import type { AnnotationTagEntity } from './AnnotationTagEntity'; import type { AnnotationTagMapping } from './AnnotationTagMapping'; import { AnnotationVote } from 'n8n-workflow'; diff --git a/packages/editor-ui/src/components/AnnotationTagsDropdown.vue b/packages/editor-ui/src/components/AnnotationTagsDropdown.vue index 37c2ced970fe4..abca50ed25b3a 100644 --- a/packages/editor-ui/src/components/AnnotationTagsDropdown.vue +++ b/packages/editor-ui/src/components/AnnotationTagsDropdown.vue @@ -4,7 +4,7 @@ import { useI18n } from '@/composables/useI18n'; import { useToast } from '@/composables/useToast'; import { useUIStore } from '@/stores/ui.store'; import { useAnnotationTagsStore } from '@/stores/tags.store'; -import { ANNOTATION_TAGS_MANAGER_MODAL_KEY, TAGS_MANAGER_MODAL_KEY } from '@/constants'; +import { ANNOTATION_TAGS_MANAGER_MODAL_KEY } from '@/constants'; import type { EventBus } from 'n8n-design-system'; interface TagsDropdownWrapperProps { diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index a2c60236f6d56..4322e65c249c2 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -15,7 +15,7 @@ :placeholder="$locale.baseText('duplicateWorkflowDialog.enterWorkflowName')" :maxlength="MAX_WORKFLOW_NAME_LENGTH" /> -