feat(editor): Show private credential icon on execution list runs (#32957)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Savelii
2026-06-30 16:06:54 +02:00
committed by GitHub
parent 871b253e9b
commit 4290e8461a
29 changed files with 123 additions and 11 deletions
+2 -1
View File
@@ -53,7 +53,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.execution_annotation_tags](public.execution_annotation_tags.md) | 2 | | BASE TABLE |
| [public.execution_annotations](public.execution_annotations.md) | 6 | | BASE TABLE |
| [public.execution_data](public.execution_data.md) | 4 | | BASE TABLE |
| [public.execution_entity](public.execution_entity.md) | 18 | | BASE TABLE |
| [public.execution_entity](public.execution_entity.md) | 19 | | BASE TABLE |
| [public.execution_metadata](public.execution_metadata.md) | 4 | | BASE TABLE |
| [public.folder](public.folder.md) | 6 | | BASE TABLE |
| [public.folder_tag](public.folder_tag.md) | 2 | | BASE TABLE |
@@ -750,6 +750,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -113,6 +113,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -61,6 +61,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -54,6 +54,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -19,6 +19,7 @@
| stoppedAt | timestamp(3) with time zone | | true | | | |
| storedAt | varchar(2) | 'db'::character varying | false | | | |
| tracingContext | json | | true | | | |
| usedPrivateCredentials | boolean | false | false | | | Whether this execution ran with at least one dynamically-resolved private credential. |
| waitTill | timestamp(3) with time zone | | true | | | |
| workflowId | varchar(36) | | false | | [public.workflow_entity](public.workflow_entity.md) | |
| workflowVersionId | varchar(36) | NULL::character varying | true | | | Version id of the workflow run by this execution; denormalized from the data bundle. |
@@ -36,6 +37,7 @@
| execution_entity_mode_not_null | n | NOT NULL mode |
| execution_entity_status_not_null | n | NOT NULL status |
| execution_entity_storedAt_not_null | n | NOT NULL "storedAt" |
| execution_entity_usedPrivateCredentials_not_null | n | NOT NULL "usedPrivateCredentials" |
| execution_entity_workflowId_not_null | n | NOT NULL "workflowId" |
| fk_execution_entity_workflow_id | FOREIGN KEY | FOREIGN KEY ("workflowId") REFERENCES workflow_entity(id) ON DELETE CASCADE |
| pk_e3e63bbf986767844bbe1166d4e | PRIMARY KEY | PRIMARY KEY (id) |
@@ -80,6 +82,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -56,6 +56,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -79,6 +79,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -202,6 +202,7 @@ erDiagram
timestamp_3__with_time_zone stoppedAt
varchar_2_ storedAt
json tracingContext
boolean usedPrivateCredentials
timestamp_3__with_time_zone waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
+2 -1
View File
@@ -53,7 +53,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [execution_annotation_tags](execution_annotation_tags.md) | 2 | | table |
| [execution_annotations](execution_annotations.md) | 6 | | table |
| [execution_data](execution_data.md) | 4 | | table |
| [execution_entity](execution_entity.md) | 18 | | table |
| [execution_entity](execution_entity.md) | 19 | | table |
| [execution_metadata](execution_metadata.md) | 4 | | table |
| [folder](folder.md) | 6 | | table |
| [folder_tag](folder_tag.md) | 2 | | table |
@@ -738,6 +738,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -117,6 +117,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -67,6 +67,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -68,6 +68,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -6,7 +6,7 @@
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "workflowId" varchar(36) NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime, "stoppedAt" datetime, "waitTill" datetime, "status" varchar NOT NULL, "deletedAt" datetime(3), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "storedAt" varchar(2) NOT NULL DEFAULT ('db'), "tracingContext" text, "deduplicationKey" varchar(255), "jsonSizeBytes" bigint NOT NULL DEFAULT (0), "workflowVersionId" varchar(36) DEFAULT (NULL), "binaryDataSizeBytes" bigint NOT NULL DEFAULT (0), CONSTRAINT "CHK_execution_entity_storedAt" CHECK ("storedAt" IN ('db', 'fs', 's3', 'az')), CONSTRAINT "FK_c4d999a5e90784e8caccf5589de" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)
CREATE TABLE "execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "workflowId" varchar(36) NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime, "stoppedAt" datetime, "waitTill" datetime, "status" varchar NOT NULL, "deletedAt" datetime(3), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "storedAt" varchar(2) NOT NULL DEFAULT ('db'), "tracingContext" text, "deduplicationKey" varchar(255), "jsonSizeBytes" bigint NOT NULL DEFAULT (0), "workflowVersionId" varchar(36) DEFAULT (NULL), "binaryDataSizeBytes" bigint NOT NULL DEFAULT (0), "usedPrivateCredentials" BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT "CHK_execution_entity_storedAt" CHECK ("storedAt" IN ('db', 'fs', 's3', 'az')), CONSTRAINT "FK_c4d999a5e90784e8caccf5589de" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)
```
</details>
@@ -30,6 +30,7 @@ CREATE TABLE "execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL
| stoppedAt | datetime | | true | | | |
| storedAt | varchar(2) | 'db' | false | | | |
| tracingContext | TEXT | | true | | | |
| usedPrivateCredentials | BOOLEAN | FALSE | false | | | |
| waitTill | datetime | | true | | | |
| workflowId | varchar(36) | | false | | [workflow_entity](workflow_entity.md) | |
| workflowVersionId | varchar(36) | NULL | true | | | |
@@ -80,6 +81,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -62,6 +62,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -94,6 +94,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -201,6 +201,7 @@ erDiagram
datetime stoppedAt
varchar_2_ storedAt
TEXT tracingContext
BOOLEAN usedPrivateCredentials
datetime waitTill
varchar_36_ workflowId FK
varchar_36_ workflowVersionId
@@ -127,6 +127,9 @@ export class ExecutionEntity {
@Column({ type: 'varchar', length: 36, nullable: true })
workflowVersionId: string | null;
@Column({ default: false })
usedPrivateCredentials: boolean;
@OneToMany('ExecutionMetadata', 'execution')
metadata: ExecutionMetadata[];
@@ -70,6 +70,7 @@ export interface IExecutionBase {
jsonSizeBytes?: number; // see `ExecutionEntity.jsonSizeBytes`
binaryDataSizeBytes?: number; // see `ExecutionEntity.binaryDataSizeBytes`
workflowVersionId?: string | null; // see `ExecutionEntity.workflowVersionId`
usedPrivateCredentials?: boolean; // see `ExecutionEntity.usedPrivateCredentials`
}
// Required by PublicUser
@@ -0,0 +1,25 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddUsedPrivateCredentialsToExecutionEntity1784000000041
implements ReversibleMigration
{
async up({ escape, runQuery, isPostgres }: MigrationContext) {
const tableName = escape.tableName('execution_entity');
const column = escape.columnName('usedPrivateCredentials');
await runQuery(`ALTER TABLE ${tableName} ADD COLUMN ${column} BOOLEAN NOT NULL DEFAULT FALSE`);
if (isPostgres) {
await runQuery(
`COMMENT ON COLUMN ${tableName}.${column} IS 'Whether this execution ran with at least one dynamically-resolved private credential.'`,
);
}
}
async down({ escape, runQuery }: MigrationContext) {
const tableName = escape.tableName('execution_entity');
const column = escape.columnName('usedPrivateCredentials');
await runQuery(`ALTER TABLE ${tableName} DROP COLUMN ${column}`);
}
}
@@ -214,6 +214,7 @@ import { DropAgentDescriptionFromAgents1784000000037 } from '../common/178400000
import { SetChatHubEnabledFromUsage1784000000038 } from '../common/1784000000038-SetChatHubEnabledFromUsage';
import { DropAgentExecutionFallbackColumns1784000000039 } from '../common/1784000000039-DropAgentExecutionFallbackColumns';
import { CreateWorkflowPublicationTriggerStatusTable1784000000040 } from '../common/1784000000040-CreateWorkflowPublicationTriggerStatusTable';
import { AddUsedPrivateCredentialsToExecutionEntity1784000000041 } from '../common/1784000000041-AddUsedPrivateCredentialsToExecutionEntity';
import type { Migration } from '../migration-types';
export const postgresMigrations: Migration[] = [
@@ -433,4 +434,5 @@ export const postgresMigrations: Migration[] = [
SetChatHubEnabledFromUsage1784000000038,
DropAgentExecutionFallbackColumns1784000000039,
CreateWorkflowPublicationTriggerStatusTable1784000000040,
AddUsedPrivateCredentialsToExecutionEntity1784000000041,
];
@@ -207,6 +207,7 @@ import { CreateInstanceAiThreadGrantTable1784000000036 } from '../common/1784000
import { SetChatHubEnabledFromUsage1784000000038 } from '../common/1784000000038-SetChatHubEnabledFromUsage';
import { DropAgentExecutionFallbackColumns1784000000039 } from '../common/1784000000039-DropAgentExecutionFallbackColumns';
import { CreateWorkflowPublicationTriggerStatusTable1784000000040 } from '../common/1784000000040-CreateWorkflowPublicationTriggerStatusTable';
import { AddUsedPrivateCredentialsToExecutionEntity1784000000041 } from '../common/1784000000041-AddUsedPrivateCredentialsToExecutionEntity';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@@ -417,6 +418,7 @@ const sqliteMigrations: Migration[] = [
SetChatHubEnabledFromUsage1784000000038,
DropAgentExecutionFallbackColumns1784000000039,
CreateWorkflowPublicationTriggerStatusTable1784000000040,
AddUsedPrivateCredentialsToExecutionEntity1784000000041,
];
export { sqliteMigrations };
@@ -753,6 +753,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
createdAt: true,
startedAt: true,
stoppedAt: true,
usedPrivateCredentials: true,
};
private annotationFields = {
@@ -833,6 +834,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
waitTill?: Date | string | null;
jsonSizeBytes?: number | string;
binaryDataSizeBytes?: number | string;
usedPrivateCredentials?: boolean | number;
}): ExecutionSummary {
execution.id = execution.id.toString();
@@ -846,6 +848,11 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
execution.binaryDataSizeBytes = Number(execution.binaryDataSizeBytes);
}
// SQLite returns 0/1 for booleans; coerce to a proper boolean.
if (typeof execution.usedPrivateCredentials === 'number') {
execution.usedPrivateCredentials = execution.usedPrivateCredentials !== 0;
}
const normalizeDateString = (date: string) => {
if (date.includes(' ')) return date.replace(' ', 'T') + 'Z';
return date;
@@ -45,6 +45,7 @@ import {
updateExistingExecution,
updateExistingExecutionMetadata,
} from './shared/shared-hook-functions';
import { type ExecutionSaveSettings, toSaveSettings } from './to-save-settings';
@Service()
@@ -603,6 +604,14 @@ function hookFunctionsSave(
fullExecutionData.data.pushRef = pushRef;
}
fullExecutionData.usedPrivateCredentials = Object.values(
fullRunData.data?.resultData?.runData ?? {},
).some((taskDataList) =>
taskDataList.some(
(t) => t.usedDynamicCredentials === true || t.attemptedDynamicCredentials === true,
),
);
await updateExistingExecution({
executionId: this.executionId,
workflowId: this.workflowData.id,
@@ -685,6 +694,14 @@ function hookFunctionsSaveWorker(
fullExecutionData.data.pushRef = pushRef;
}
fullExecutionData.usedPrivateCredentials = Object.values(
fullRunData.data?.resultData?.runData ?? {},
).some((taskDataList) =>
taskDataList.some(
(t) => t.usedDynamicCredentials === true || t.attemptedDynamicCredentials === true,
),
);
// In scaling mode, worker saves execution without metadata
// Main process will save metadata after deletion decisions to avoid FK violations
await updateExistingExecution({
@@ -100,6 +100,7 @@ describe('ExecutionService', () => {
workflowVersionId: null,
jsonSizeBytes: 0,
binaryDataSizeBytes: 0,
usedPrivateCredentials: false,
annotation: {
tags: expect.arrayContaining([]),
vote: null,
@@ -966,6 +967,7 @@ describe('ExecutionService', () => {
workflowVersionId: null,
jsonSizeBytes: 0,
binaryDataSizeBytes: 0,
usedPrivateCredentials: false,
};
afterEach(async () => {
@@ -1314,6 +1314,7 @@
"error.entityUnAuthorized.title": "You need access",
"error.entityUnAuthorized.content": "You don't have permission to view this {entity}. Please contact the person who shared this link to request access.",
"executions.ExecutionStatus": "Execution status",
"executions.privateCredential.tooltip": "This execution ran using end-user credentials",
"executions.concurrency.docsLink": "https://docs.n8n.io/hosting/scaling/concurrency-control/",
"executionDetails.additionalActions": "Additional Actions",
"executionDetails.confirmMessage.confirmButtonText": "Yes, delete",
@@ -23,6 +23,7 @@ import {
N8nText,
N8nTooltip,
} from '@n8n/design-system';
import PrivateCredentialIcon from '@/features/resolvers/components/PrivateCredentialIcon.vue';
type Command = 'retrySaved' | 'retryOriginal' | 'delete';
const emit = defineEmits<{
@@ -274,13 +275,18 @@ async function handleActionItemClick(commandData: Command) {
</small>
</span>
</td>
<td>
<td :class="$style.modeCell">
<N8nTooltip v-if="execution.mode === 'manual'" content="Manual Execution" placement="top">
<N8nIcon icon="flask-conical" />
</N8nTooltip>
<N8nTooltip v-else-if="execution.mode === 'chat'" content="Chat Execution" placement="top">
<N8nIcon icon="messages-square" />
</N8nTooltip>
<PrivateCredentialIcon
v-if="execution.usedPrivateCredentials"
data-test-id="global-execution-private-credential"
:tooltip-text="locale.baseText('executions.privateCredential.tooltip')"
/>
</td>
<td>
<N8nButton
@@ -338,6 +344,19 @@ async function handleActionItemClick(commandData: Command) {
</template>
<style lang="scss" module>
.modeCell {
display: flex;
align-items: center;
gap: var(--spacing--2xs);
/* Normalize icon wrappers so SVG baseline doesn't shift relative to tooltip spans */
:deep(svg),
:deep(span) {
display: inline-flex;
align-items: center;
}
}
tr.dangerBg {
background-color: rgba(215, 56, 58, 0.1);
}
@@ -11,7 +11,7 @@ import { useI18n } from '@n8n/i18n';
import type { PermissionsRecord } from '@n8n/permissions';
import { useSettingsStore } from '@/app/stores/settings.store';
import { toDayMonth, toTime } from '@/app/utils/formatters/dateFormatter';
import PrivateCredentialIcon from '@/features/resolvers/components/PrivateCredentialIcon.vue';
import {
N8nActionDropdown,
N8nIcon,
@@ -44,6 +44,8 @@ const isAdvancedExecutionFilterEnabled = computed(
const isAnnotationEnabled = computed(() => isAdvancedExecutionFilterEnabled.value);
const workflowId = useInjectWorkflowId();
const hasPrivateCredentials = computed(() => props.execution.usedPrivateCredentials === true);
const retryExecutionActions = computed(() => [
{
id: 'current-workflow',
@@ -169,6 +171,12 @@ function onRetryMenuItemSelect(action: string): void {
data-test-id="retry-execution-button"
@select="onRetryMenuItemSelect"
/>
<PrivateCredentialIcon
v-if="hasPrivateCredentials"
:class="$style.icon"
:tooltip-text="locale.baseText('executions.privateCredential.tooltip')"
data-test-id="execution-card-private-credential"
/>
<N8nTooltip v-if="execution.mode === 'manual'" placement="top">
<template #content>
<span>{{ locale.baseText('executionsList.test') }}</span>
@@ -322,10 +330,17 @@ function onRetryMenuItemSelect(action: string): void {
.icons {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing--2xs);
min-width: calc(2 * var(--font-size--sm) + var(--spacing--2xs));
}
.icon {
font-size: var(--font-size--sm);
width: var(--font-size--sm);
display: inline-flex;
align-items: center;
justify-content: center;
&.retry {
svg {
@@ -337,10 +352,6 @@ function onRetryMenuItemSelect(action: string): void {
position: relative;
top: 1px;
}
& + & {
margin-left: var(--spacing--2xs);
}
}
.showGap {
@@ -4,6 +4,8 @@ import { computed } from 'vue';
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
defineOptions({ inheritAttrs: false });
interface Props {
tooltip?: boolean;
tooltipText?: string;
@@ -20,8 +22,8 @@ const content = computed(() => props.tooltipText ?? i18n.baseText('credentials.p
</script>
<template>
<N8nTooltip :disabled="!tooltip" placement="top">
<N8nTooltip as-child :disabled="!tooltip" placement="top">
<template #content>{{ content }}</template>
<N8nIcon data-test-id="private-credential-icon" icon="user-round-key" size="small" />
<N8nIcon v-bind="$attrs" data-test-id="private-credential-icon" icon="user-round-key" />
</N8nTooltip>
</template>
+1
View File
@@ -3736,6 +3736,7 @@ export interface ExecutionSummary {
nodeExecutionStatus?: {
[key: string]: IExecutionSummaryNodeExecutionResult;
};
usedPrivateCredentials?: boolean;
annotation?: {
vote: AnnotationVote;
tags: Array<{