feat(editor): Add reusable PrivateCredentialIcon component (#32866)

This commit is contained in:
Savelii
2026-06-24 14:25:56 +00:00
committed by GitHub
parent cde94d948f
commit 0d009122e9
11 changed files with 95 additions and 52 deletions
@@ -23,7 +23,7 @@
"lint:styles:fix": "stylelint \"src/**/*.{scss,sass,vue}\" --fix --cache"
},
"devDependencies": {
"@iconify/json": "^2.2.349",
"@iconify/json": "^2.2.488",
"@n8n/eslint-config": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/stylelint-config": "workspace:*",
@@ -202,6 +202,11 @@ export const AllColors: StoryFn = (args, { argTypes }) => ({
`,
});
export const UserRoundKey = Template.bind({});
UserRoundKey.args = {
icon: 'user-round-key',
};
export const CommonIcons: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
@@ -258,6 +263,10 @@ export const CommonIcons: StoryFn = (args, { argTypes }) => ({
<n8n-icon icon="exclamation-triangle" size="large" />
<span style="font-size: 12px;">exclamation-triangle</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<n8n-icon icon="user-round-key" size="large" />
<span style="font-size: 12px;">user-round-key</span>
</div>
</div>
`,
});
@@ -260,6 +260,7 @@ import IconLucideUserCheck from '~icons/lucide/user-check';
import IconLucideUserLock from '~icons/lucide/user-lock';
import IconLucideUserPen from '~icons/lucide/user-pen';
import IconLucideUserRound from '~icons/lucide/user-round';
import IconLucideUserRoundKey from '~icons/lucide/user-round-key';
import IconLucideUsers from '~icons/lucide/users';
import IconLucideVariable from '~icons/lucide/variable';
import IconLucideVault from '~icons/lucide/vault';
@@ -752,6 +753,7 @@ export const updatedIconSet = {
'user-lock': IconLucideUserLock,
'user-pen': IconLucideUserPen,
'user-round': IconLucideUserRound,
'user-round-key': IconLucideUserRoundKey,
users: IconLucideUsers,
vault: IconLucideVault,
video: IconLucideVideo,
@@ -4347,6 +4347,7 @@
"workflows.item.readonly": "Read only",
"workflows.item.archived": "Archived",
"workflows.item.availableInMCP": "Available in MCP",
"workflows.privateCredential.tooltip": "Set up and run with your own credentials",
"workflows.dynamic.tooltip": "This workflow resolves credentials at runtime based on who executes it.",
"workflows.dynamic.tooltipTitle": "Private credentials",
"workflows.dependencies.tooltip.workflow": "Click to view resources referenced by this workflow",
+1 -1
View File
@@ -126,7 +126,7 @@
"devDependencies": {
"@codecov/vite-plugin": "^1.9.1",
"@faker-js/faker": "^8.0.2",
"@iconify/json": "^2.2.349",
"@iconify/json": "^2.2.488",
"@n8n/eslint-config": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/stylelint-config": "workspace:*",
@@ -1094,25 +1094,25 @@ describe('WorkflowCard', () => {
expect(queryByTestId('workflow-card-mcp-toggle')).not.toBeInTheDocument();
});
it('should show dynamic credentials indicator when workflow has resolvable credentials', () => {
it('should show private credential indicator when workflow has resolvable credentials', () => {
const data = createWorkflow({
hasResolvableCredentials: true,
});
const { getByTestId } = renderComponent({ props: { data } });
const indicator = getByTestId('workflow-card-dynamic-credentials');
const indicator = getByTestId('workflow-card-private-credential');
expect(indicator).toBeVisible();
});
it('should hide dynamic credentials indicator when workflow has no resolvable credentials', () => {
it('should hide private credential indicator when workflow has no resolvable credentials', () => {
const data = createWorkflow({
hasResolvableCredentials: false,
});
const { queryByTestId } = renderComponent({ props: { data } });
const indicator = queryByTestId('workflow-card-dynamic-credentials');
const indicator = queryByTestId('workflow-card-private-credential');
expect(indicator).toBeNull();
});
@@ -50,6 +50,7 @@ import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate';
import { createEventBus } from '@n8n/utils/event-bus';
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
import PrivateCredentialIcon from '@/features/resolvers/components/PrivateCredentialIcon.vue';
import { useDependencies } from '@/app/composables/useDependencies';
const WORKFLOW_LIST_ITEM_ACTIONS = {
@@ -618,24 +619,6 @@ const tags = computed(
<N8nBadge v-if="!workflowPermissions.update" class="ml-3xs" theme="tertiary" bold>
{{ locale.baseText('workflows.item.readonly') }}
</N8nBadge>
<N8nTooltip v-if="hasDynamicCredentials" placement="top">
<template #content>
<div :class="$style.tooltipContent">
<strong>{{ locale.baseText('workflows.dynamic.tooltipTitle') }}</strong>
<span>{{ locale.baseText('workflows.dynamic.tooltip') }}</span>
</div>
</template>
<N8nBadge
theme="tertiary"
class="ml-3xs pl-3xs pr-3xs"
data-test-id="workflow-card-dynamic-credentials"
>
<span :class="$style.dynamicBadgeText">
<N8nIcon icon="key-round" size="medium" />
{{ locale.baseText('credentials.private.badge') }}
</span>
</N8nBadge>
</N8nTooltip>
</N8nText>
</template>
<div :class="$style.cardDescription">
@@ -656,6 +639,17 @@ const tags = computed(
<N8nIcon icon="mcp" size="medium" />
</N8nTooltip>
</span>
<template v-if="hasDynamicCredentials">
<span>|</span>
<span
:class="$style.privateCredentialIndicator"
data-test-id="workflow-card-private-credential"
>
<PrivateCredentialIcon
:tooltip-text="locale.baseText('workflows.privateCredential.tooltip')"
/>
</span>
</template>
<span
v-if="props.areTagsEnabled && data.tags && data.tags.length > 0"
v-show="data"
@@ -796,6 +790,11 @@ const tags = computed(
align-items: center;
}
.privateCredentialIndicator {
display: inline-flex;
align-items: center;
}
.cardActions {
display: flex;
gap: var(--spacing--2xs);
@@ -826,20 +825,6 @@ const tags = computed(
color: var(--color--text);
}
.dynamicBadgeText {
display: inline-flex;
align-items: center;
gap: var(--spacing--4xs);
font-size: var(--font-size--3xs);
height: 18px;
}
.tooltipContent {
display: flex;
flex-direction: column;
gap: var(--spacing--4xs);
}
.publishIndicator {
display: flex;
align-items: center;
@@ -26,6 +26,8 @@ const uiStore = useUIStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const instanceAiCredentialHelp = useInstanceAiCredentialHelp();
const searchQuery = ref('');
onMounted(async () => {
try {
await credentialsStore.fetchCredentialTypes(false);
@@ -41,10 +43,20 @@ onMounted(async () => {
});
// Exclude purpose built credentials for ChatHub
const selectableCredentialTypes = computed(() =>
const allSelectableCredentialTypes = computed(() =>
credentialsStore.allCredentialTypes.filter((c) => !c.name.startsWith('chatHub')),
);
const selectableCredentialTypes = computed(() => {
if (!searchQuery.value) return allSelectableCredentialTypes.value;
const q = searchQuery.value.toLowerCase();
return allSelectableCredentialTypes.value.filter((c) => c.displayName.toLowerCase().includes(q));
});
function filterCredentials(query: string) {
searchQuery.value = query;
}
function onSelect(type: string) {
selected.value = type;
}
@@ -106,6 +118,7 @@ function openCredentialType() {
:placeholder="i18n.baseText('credentialSelectModal.searchForApp')"
size="xlarge"
:model-value="selected"
:filter-method="filterCredentials"
data-test-id="new-credential-type-select"
@update:model-value="onSelect"
>
@@ -0,0 +1,11 @@
import { createComponentRenderer } from '@/__tests__/render';
import PrivateCredentialIcon from './PrivateCredentialIcon.vue';
const renderComponent = createComponentRenderer(PrivateCredentialIcon);
describe('PrivateCredentialIcon', () => {
it('should render the icon', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('private-credential-icon')).toBeInTheDocument();
});
});
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { computed } from 'vue';
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
interface Props {
tooltip?: boolean;
tooltipText?: string;
}
const props = withDefaults(defineProps<Props>(), {
tooltip: true,
tooltipText: undefined,
});
const i18n = useI18n();
const content = computed(() => props.tooltipText ?? i18n.baseText('credentials.private.tooltip'));
</script>
<template>
<N8nTooltip :disabled="!tooltip" placement="top">
<template #content>{{ content }}</template>
<N8nIcon data-test-id="private-credential-icon" icon="user-round-key" size="small" />
</N8nTooltip>
</template>
+8 -13
View File
@@ -4188,8 +4188,8 @@ importers:
version: 1.0.15
devDependencies:
'@iconify/json':
specifier: ^2.2.349
version: 2.2.354
specifier: ^2.2.488
version: 2.2.488
'@n8n/eslint-config':
specifier: workspace:*
version: link:../../../@n8n/eslint-config
@@ -4853,8 +4853,8 @@ importers:
specifier: ^8.0.2
version: 8.4.1
'@iconify/json':
specifier: ^2.2.349
version: 2.2.354
specifier: ^2.2.488
version: 2.2.488
'@n8n/eslint-config':
specifier: workspace:*
version: link:../../@n8n/eslint-config
@@ -7620,8 +7620,8 @@ packages:
'@iconify-json/mdi@1.1.63':
resolution: {integrity: sha512-b07dRM5LYdwJqd/lm+1+JDNDNSWHvpZ7ujb73oJxx3lmK1J0giDS0BSc4yia0hixNV0Z2q/4GyxAu/A8XqDnbg==}
'@iconify/json@2.2.354':
resolution: {integrity: sha512-I7sYXaLfvGw0msuc5p2VdQJ4nthbsqLW8IF9j1jaekUcs/xwGVgs3SHO5lEiFglv0odlkCu26CMbboZMvwkTcw==}
'@iconify/json@2.2.488':
resolution: {integrity: sha512-W2MfGS0n1GugWhQn7SLgRKNH1JonVsKG/O9vO/htP7j3k1bGt54R1mbN9uaZlQ99uCCu65IR5UgQM66E8Cm+Fw==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -18234,9 +18234,6 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -24875,10 +24872,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify/json@2.2.354':
'@iconify/json@2.2.488':
dependencies:
'@iconify/types': 2.0.0
pathe: 1.1.2
pathe: 2.0.3
'@iconify/types@2.0.0': {}
@@ -37338,8 +37335,6 @@ snapshots:
path-type@4.0.0: {}
pathe@1.1.2: {}
pathe@2.0.3: {}
pathval@2.0.0: {}