mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(editor): First shot at adding a dynamic credential section on the credential modal (#22983)
This commit is contained in:
@@ -791,6 +791,12 @@
|
||||
"credentialEdit.credentialConfig.recommendedAuthTypeSuffix": "(recommended)",
|
||||
"credentialEdit.credentialConfig.externalSecrets": "Enterprise plan users can pull in credentials from external vaults.",
|
||||
"credentialEdit.credentialConfig.externalSecrets.moreInfo": "More info",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.title": "Set up for dynamic credentials",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.infoTip": "Enable this credential to be used with workflow resolvers",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.description1": "Requires a workflow resolver in Workflow settings.",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.description2": "At run time it uses that resolver to select credentials based on who triggered the workflow.",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.documentation": "Documentation",
|
||||
"credentialEdit.credentialConfig.dynamicCredentials.docsUrl": "https://docs.n8n.io/credentials/dynamic-credentials/",
|
||||
"credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText": "Close",
|
||||
"credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText": "Keep Editing",
|
||||
"credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline": "Close without saving?",
|
||||
|
||||
+108
@@ -158,4 +158,112 @@ describe('CredentialConfig', () => {
|
||||
// Verify that addCredentialTranslation was not called
|
||||
expect(addCredentialTranslation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Dynamic Credentials Section', () => {
|
||||
it('should not display dynamic credentials section when feature flag is disabled', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
enterprise: {
|
||||
sharing: false,
|
||||
externalSecrets: false,
|
||||
},
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent(
|
||||
{
|
||||
props: {
|
||||
isManaged: false,
|
||||
mode: 'new',
|
||||
credentialPermissions: {
|
||||
...defaultRenderOptions.props!.credentialPermissions,
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('dynamic-credentials-section')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display dynamic credentials section when user lacks permissions', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
enterprise: {
|
||||
sharing: false,
|
||||
externalSecrets: false,
|
||||
},
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent(
|
||||
{
|
||||
props: {
|
||||
isManaged: false,
|
||||
mode: 'edit',
|
||||
credentialPermissions: {
|
||||
...defaultRenderOptions.props!.credentialPermissions,
|
||||
create: false,
|
||||
update: false,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('dynamic-credentials-section')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the toggle switch in the dynamic credentials section', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
enterprise: {
|
||||
sharing: false,
|
||||
externalSecrets: false,
|
||||
},
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent(
|
||||
{
|
||||
props: {
|
||||
isManaged: false,
|
||||
mode: 'new',
|
||||
credentialPermissions: {
|
||||
...defaultRenderOptions.props!.credentialPermissions,
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('dynamic-credentials-toggle')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+77
@@ -34,15 +34,19 @@ import OauthButton from './OauthButton.vue';
|
||||
import { useChatPanelStore } from '@/features/ai/assistant/chatPanel.store';
|
||||
import { useAssistantStore } from '@/features/ai/assistant/assistant.store';
|
||||
import FreeAiCreditsCallout from '@/app/components/FreeAiCreditsCallout.vue';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
|
||||
import {
|
||||
N8nCallout,
|
||||
N8nIcon,
|
||||
N8nInfoTip,
|
||||
N8nInlineAskAssistantButton,
|
||||
N8nLink,
|
||||
N8nNotice,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
} from '@n8n/design-system';
|
||||
import { ElSwitch } from 'element-plus';
|
||||
|
||||
type Props = {
|
||||
mode: string;
|
||||
@@ -89,6 +93,7 @@ const chatPanelStore = useChatPanelStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
uiStore.activeCredentialType = props.credentialType.name;
|
||||
@@ -195,6 +200,14 @@ const assistantAlreadyAsked = computed<boolean>(() => {
|
||||
return assistantStore.isCredTypeActive(props.credentialType);
|
||||
});
|
||||
|
||||
const isResolvable = computed<boolean>(() => {
|
||||
return Boolean(props.credentialData.isResolvable);
|
||||
});
|
||||
|
||||
const isDynamicCredentialsEnabled = computed<boolean>(() => {
|
||||
return checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS');
|
||||
});
|
||||
|
||||
function onDataChange(event: IUpdateInformation): void {
|
||||
emit('update', event);
|
||||
}
|
||||
@@ -402,6 +415,52 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
||||
</N8nInfoTip>
|
||||
</template>
|
||||
</EnterpriseEdition>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
isDynamicCredentialsEnabled &&
|
||||
((credentialPermissions.create && isNewCredential) || credentialPermissions.update)
|
||||
"
|
||||
:class="$style.dynamicCredentials"
|
||||
data-test-id="dynamic-credentials-section"
|
||||
>
|
||||
<div :class="$style.dynamicCredentialsHeader">
|
||||
<N8nText size="medium" weight="bold">
|
||||
{{ i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.title') }}
|
||||
</N8nText>
|
||||
<N8nTooltip placement="top">
|
||||
<template #content>
|
||||
<div>
|
||||
{{ i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.infoTip') }}
|
||||
</div>
|
||||
</template>
|
||||
<N8nIcon icon="circle-help" size="small" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<ElSwitch
|
||||
:model-value="isResolvable"
|
||||
data-test-id="dynamic-credentials-toggle"
|
||||
@update:model-value="onDataChange({ name: 'isResolvable', value: $event })"
|
||||
/>
|
||||
<div :class="$style.dynamicCredentialsDescription">
|
||||
<N8nText :tag="'div'" size="small" color="text-light">
|
||||
{{ i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.description1') }}
|
||||
</N8nText>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.description2') }}
|
||||
<N8nLink
|
||||
:to="i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.docsUrl')"
|
||||
size="small"
|
||||
theme="text"
|
||||
underline
|
||||
>
|
||||
{{
|
||||
i18n.baseText('credentialEdit.credentialConfig.dynamicCredentials.documentation')
|
||||
}}
|
||||
</N8nLink>
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -429,4 +488,22 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
||||
font-size: var(--font-size--sm);
|
||||
}
|
||||
}
|
||||
|
||||
.dynamicCredentials {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--xs);
|
||||
padding-top: var(--spacing--lg);
|
||||
border-top: var(--border);
|
||||
}
|
||||
|
||||
.dynamicCredentialsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.dynamicCredentialsDescription {
|
||||
margin-top: var(--spacing--2xs);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface ICredentialsResponse extends ICredentialsEncrypted {
|
||||
ownedBy?: Pick<IUserResponse, 'id' | 'firstName' | 'lastName' | 'email'>;
|
||||
isManaged: boolean;
|
||||
isGlobal?: boolean;
|
||||
isResolvable?: boolean;
|
||||
}
|
||||
|
||||
export interface IUsedCredential {
|
||||
@@ -33,6 +34,7 @@ export interface ICredentialsBase {
|
||||
|
||||
export interface ICredentialsDecryptedResponse extends ICredentialsBase, ICredentialsDecrypted {
|
||||
id: string;
|
||||
isResolvable?: boolean;
|
||||
}
|
||||
|
||||
export interface ICredentialTypeMap {
|
||||
|
||||
Reference in New Issue
Block a user