refactor: Drop called-by column and align trigger view controls

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Charlie Kolb 2026-08-12 13:21:18 +02:00
parent 839730bba7
commit d72f7df53b
No known key found for this signature in database
6 changed files with 9 additions and 112 deletions

View File

@ -1,6 +1,6 @@
import { DatabaseConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import { DataSource, EntityManager, In, IsNull, LessThan, Repository, Not } from '@n8n/typeorm';
import { DataSource, EntityManager, IsNull, LessThan, Repository, Not } from '@n8n/typeorm';
import { WorkflowDependency } from '../entities';
@ -159,17 +159,4 @@ export class WorkflowDependencyRepository extends Repository<WorkflowDependency>
const { tablePrefix } = this.databaseConfig;
return this.manager.connection.driver.escape(`${tablePrefix}${name}`);
}
/**
* Retrieve the caller → callee edges of workflows that reference any of the
* given workflows via an Execute Workflow node. `workflowId` is the caller,
* `dependencyKey` the called workflow's id.
*/
async findCallersOfWorkflows(workflowIds: string[]) {
if (workflowIds.length === 0) return [];
return await this.find({
select: ['workflowId', 'dependencyKey'],
where: { dependencyType: 'workflowCall', dependencyKey: In(workflowIds) },
});
}
}

View File

@ -2,7 +2,6 @@ import { WebhookEntity } from '@n8n/db';
import type {
Project,
SharedWorkflowRepository,
WorkflowDependencyRepository,
WorkflowEntity,
WorkflowRepository,
} from '@n8n/db';
@ -29,13 +28,11 @@ describe('WebhooksController', () => {
const webhookService = mock<WebhookService>();
const workflowRepository = mock<WorkflowRepository>();
const sharedWorkflowRepository = mock<SharedWorkflowRepository>();
const workflowDependencyRepository = mock<WorkflowDependencyRepository>();
const nodeTypes = mock<NodeTypes>();
const controller = new WebhooksController(
webhookService,
workflowRepository,
sharedWorkflowRepository,
workflowDependencyRepository,
nodeTypes,
);
@ -46,7 +43,6 @@ describe('WebhooksController', () => {
workflowRepository.findPublishedWithActiveVersionNodes.mockResolvedValue([]);
workflowRepository.findUnpublishedWithNodes.mockResolvedValue([]);
sharedWorkflowRepository.findOwnerProjectsByWorkflowIds.mockResolvedValue(new Map());
workflowDependencyRepository.findCallersOfWorkflows.mockResolvedValue([]);
vi.mocked(getTriggerKinds).mockReturnValue(new Map());
});
@ -106,7 +102,6 @@ describe('WebhooksController', () => {
nodeType: 'n8n-nodes-base.webhook',
isActive: true,
project: { name: 'My Project', type: 'team', icon: { type: 'icon', value: 'folder' } },
calledBy: [],
},
{
kind: 'webhook',
@ -118,7 +113,6 @@ describe('WebhooksController', () => {
nodeType: undefined,
isActive: true,
project: undefined,
calledBy: [],
},
]);
});
@ -152,14 +146,6 @@ describe('WebhooksController', () => {
['n3', 'persisted'],
]),
);
// Duplicate edges: draft and published versions index separately
workflowDependencyRepository.findCallersOfWorkflows.mockResolvedValue([
{ workflowId: 'caller1', dependencyKey: 'wf5' },
{ workflowId: 'caller1', dependencyKey: 'wf5' },
] as never);
workflowRepository.findByIds.mockResolvedValue([
{ id: 'caller1', name: 'Caller Workflow' } as WorkflowEntity,
]);
const result = await controller.getAll();
@ -172,7 +158,6 @@ describe('WebhooksController', () => {
nodeType: 'n8n-nodes-base.scheduleTrigger',
isActive: true,
project: undefined,
calledBy: [{ id: 'caller1', name: 'Caller Workflow' }],
},
{
kind: 'trigger',
@ -182,7 +167,6 @@ describe('WebhooksController', () => {
nodeType: 'n8n-nodes-base.executeWorkflowTrigger',
isActive: true,
project: undefined,
calledBy: [{ id: 'caller1', name: 'Caller Workflow' }],
},
]);
});
@ -227,7 +211,6 @@ describe('WebhooksController', () => {
nodeType: 'n8n-nodes-base.scheduleTrigger',
isActive: false,
project: undefined,
calledBy: [],
},
{
kind: 'webhook',
@ -239,7 +222,6 @@ describe('WebhooksController', () => {
nodeType: 'n8n-nodes-base.webhook',
isActive: false,
project: undefined,
calledBy: [],
},
]);
});

View File

@ -1,9 +1,5 @@
import type { WorkflowEntity } from '@n8n/db';
import {
SharedWorkflowRepository,
WorkflowDependencyRepository,
WorkflowRepository,
} from '@n8n/db';
import { SharedWorkflowRepository, WorkflowRepository } from '@n8n/db';
import { Get, GlobalScope, Post, RestController } from '@n8n/decorators';
import { Request } from 'express';
import get from 'lodash/get';
@ -35,7 +31,6 @@ export class WebhooksController {
private readonly webhookService: WebhookService,
private readonly workflowRepository: WorkflowRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowDependencyRepository: WorkflowDependencyRepository,
private readonly nodeTypes: NodeTypes,
) {}
@ -52,7 +47,6 @@ export class WebhooksController {
const workflowIds = [...new Set(rows.map((row) => row.workflowId))];
const ownerProjects =
await this.sharedWorkflowRepository.findOwnerProjectsByWorkflowIds(workflowIds);
const callersByCallee = await this.getCallersByCallee(workflowIds);
return rows.map((row) => {
const project = ownerProjects.get(row.workflowId);
@ -61,34 +55,10 @@ export class WebhooksController {
project: project
? { name: project.name, type: project.type, icon: project.icon }
: undefined,
calledBy: callersByCallee.get(row.workflowId) ?? [],
};
});
}
/** Workflows calling each given workflow via an Execute Workflow node. */
private async getCallersByCallee(workflowIds: string[]) {
const edges = await this.workflowDependencyRepository.findCallersOfWorkflows(workflowIds);
const callerIds = [...new Set(edges.map((edge) => edge.workflowId))];
const callers = await this.workflowRepository.findByIds(callerIds, { fields: ['id', 'name'] });
const callerNames = new Map(callers.map((workflow) => [workflow.id, workflow.name]));
// Draft and published versions index separately, so dedupe callers per callee
const callersByCallee = new Map<string, Map<string, string>>();
for (const edge of edges) {
const perCallee = callersByCallee.get(edge.dependencyKey) ?? new Map<string, string>();
perCallee.set(edge.workflowId, callerNames.get(edge.workflowId) ?? edge.workflowId);
callersByCallee.set(edge.dependencyKey, perCallee);
}
return new Map(
[...callersByCallee.entries()].map(([calleeId, perCallee]) => [
calleeId,
[...perCallee.entries()].map(([id, name]) => ({ id, name })),
]),
);
}
private async getActiveWebhooks(): Promise<TriggerRow[]> {
const registered = await this.webhookService.findAll();
const workflowIds = [...new Set(registered.map((webhook) => webhook.workflowId))];

View File

@ -5585,7 +5585,6 @@
"settings.webhooks.column.project": "Project",
"settings.webhooks.column.node": "Node",
"settings.webhooks.column.calls": "Calls",
"settings.webhooks.column.calledBy": "Called by",
"settings.webhooks.copy.tooltip": "Copy webhook URL",
"settings.webhooks.copy.success": "Webhook URL copied",
"settings.webhooks.empty.title": "No triggers",

View File

@ -24,7 +24,6 @@ export type InstanceWebhook = {
type: 'personal' | 'team';
icon: { type: 'emoji' | 'icon'; value: string } | null;
};
calledBy: Array<{ id: string; name: string }>;
};
export const findWebhook = async (

View File

@ -9,7 +9,6 @@ import {
N8nInput,
N8nOption,
N8nSelect,
N8nTag,
N8nText,
} from '@n8n/design-system';
import type { IconOrEmoji, TableHeader } from '@n8n/design-system';
@ -71,12 +70,6 @@ const headers = computed<Array<TableHeader<WebhookRow>>>(() => [
width: 90,
align: 'end',
},
{
title: i18n.baseText('settings.webhooks.column.calledBy'),
key: 'calledBy',
width: 200,
disableSort: true,
},
]);
const nodeTypeFilter = ref<string[]>([]);
@ -104,14 +97,9 @@ const filteredItems = computed(() => {
}
if (term) {
items = items.filter((row) =>
[
row.path ?? '',
row.workflowName ?? '',
row.node,
row.method ?? '',
row.projectName,
...row.calledBy.map((caller) => caller.name),
].some((value) => value.toLowerCase().includes(term)),
[row.path ?? '', row.workflowName ?? '', row.node, row.method ?? '', row.projectName].some(
(value) => value.toLowerCase().includes(term),
),
);
}
@ -166,13 +154,6 @@ const getPathAbbreviation = (path: string) => {
return shortened === path ? path : `${shortened}…`;
};
const MAX_CALLER_PILLS = 3;
const getOverflowCallers = (row: WebhookRow) =>
row.calledBy
.slice(MAX_CALLER_PILLS)
.map((caller) => caller.name)
.join(', ');
const FLASH_DURATION_MS = 2000;
const flashedKeys = ref(new Set<string>());
const rowKey = (row: { workflowId: string; node: string; method?: string; path?: string }) =>
@ -262,6 +243,7 @@ onMounted(async () => {
multiple
collapse-tags
clearable
size="medium"
:placeholder="i18n.baseText('settings.webhooks.filter.nodeType')"
data-testid="webhooks-node-type-filter"
:class="$style.nodeTypeFilter"
@ -275,6 +257,7 @@ onMounted(async () => {
</N8nSelect>
<N8nInput
v-model="search"
size="medium"
:placeholder="i18n.baseText('settings.webhooks.search.placeholder')"
clearable
data-testid="webhooks-search"
@ -341,25 +324,6 @@ onMounted(async () => {
<N8nText :class="$style.ellipsis">{{ item.node }}</N8nText>
</div>
</template>
<template #[`item.calledBy`]="{ item }">
<div v-if="item.calledBy.length > 0" :class="$style.pillCell">
<RouterLink
v-for="caller in item.calledBy.slice(0, MAX_CALLER_PILLS)"
:key="caller.id"
:to="getWorkflowLink(caller.id)"
target="_blank"
>
<N8nTag :text="caller.name" />
</RouterLink>
<N8nTag
v-if="item.calledBy.length > MAX_CALLER_PILLS"
:text="`+${item.calledBy.length - MAX_CALLER_PILLS}`"
:clickable="false"
:title="getOverflowCallers(item)"
/>
</div>
</template>
</N8nDataTableServer>
</section>
</template>
@ -392,6 +356,8 @@ onMounted(async () => {
.controls {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing--2xs);
}
.search {
@ -426,12 +392,6 @@ onMounted(async () => {
gap: var(--spacing--3xs);
}
.pillCell {
display: flex;
flex-wrap: wrap;
gap: var(--spacing--3xs);
}
.ellipsis {
white-space: nowrap;
overflow: hidden;