mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-30 19:35:36 +08:00
feat(editor): Add reusable PrivateCredentialIcon component (#32866)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
+14
-1
@@ -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"
|
||||
>
|
||||
|
||||
+11
@@ -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();
|
||||
});
|
||||
});
|
||||
+27
@@ -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>
|
||||
Generated
+8
-13
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user