mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
fix(editor): Load only personal credentials when setting up a template (#12826)
This commit is contained in:
parent
b17cbec3af
commit
814e2a8924
|
@ -0,0 +1,276 @@
|
||||||
|
import type { ICredentialMap, ICredentialTypeMap } from '@/Interface';
|
||||||
|
|
||||||
|
export const TEST_CREDENTIALS: ICredentialMap = {
|
||||||
|
// OpenAI credential in personal
|
||||||
|
1: {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
id: '1',
|
||||||
|
name: 'OpenAi account',
|
||||||
|
data: 'test123',
|
||||||
|
type: 'openAiApi',
|
||||||
|
isManaged: false,
|
||||||
|
homeProject: {
|
||||||
|
id: '1',
|
||||||
|
type: 'personal',
|
||||||
|
name: 'Kobi Dog <kobi@n8n.io>',
|
||||||
|
icon: null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
sharedWithProjects: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Test Project',
|
||||||
|
icon: { type: 'icon', value: 'exchange-alt' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scopes: [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:list',
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:share',
|
||||||
|
'credential:update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Supabase credential in another project
|
||||||
|
2: {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
id: '2',
|
||||||
|
name: 'Supabase account',
|
||||||
|
data: 'test123',
|
||||||
|
type: 'supabaseApi',
|
||||||
|
isManaged: false,
|
||||||
|
homeProject: {
|
||||||
|
id: '2',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Test Project',
|
||||||
|
icon: { type: 'icon', value: 'exchange-alt' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
scopes: [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:list',
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:share',
|
||||||
|
'credential:update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Slack account in personal
|
||||||
|
3: {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
id: '3',
|
||||||
|
name: 'Slack account',
|
||||||
|
data: 'test123',
|
||||||
|
type: 'slackOAuth2Api',
|
||||||
|
isManaged: false,
|
||||||
|
homeProject: {
|
||||||
|
id: '1',
|
||||||
|
type: 'personal',
|
||||||
|
name: 'Kobi Dog <kobi@n8n.io>',
|
||||||
|
icon: null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
scopes: [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:list',
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:share',
|
||||||
|
'credential:update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// OpenAI credential in another project
|
||||||
|
4: {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
id: '4',
|
||||||
|
name: '[PROJECT] OpenAI Account',
|
||||||
|
data: 'test123',
|
||||||
|
type: 'openAiApi',
|
||||||
|
isManaged: false,
|
||||||
|
homeProject: {
|
||||||
|
id: '2',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Test Project',
|
||||||
|
icon: { type: 'icon', value: 'exchange-alt' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
scopes: [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:list',
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:share',
|
||||||
|
'credential:update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEST_CREDENTIAL_TYPES: ICredentialTypeMap = {
|
||||||
|
openAiApi: {
|
||||||
|
name: 'openAiApi',
|
||||||
|
displayName: 'OpenAi',
|
||||||
|
documentationUrl: 'openAi',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'API Key',
|
||||||
|
name: 'apiKey',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Base URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
default: 'https://api.openai.com/v1',
|
||||||
|
description: 'Override the base URL for the API',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authenticate: {
|
||||||
|
type: 'generic',
|
||||||
|
properties: {
|
||||||
|
headers: {
|
||||||
|
Authorization: '=Bearer {{$credentials.apiKey}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
request: {
|
||||||
|
baseURL: '={{$credentials?.url}}',
|
||||||
|
url: '/models',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iconUrl: {
|
||||||
|
light: 'icons/n8n-nodes-base/dist/nodes/OpenAi/openai.svg',
|
||||||
|
dark: 'icons/n8n-nodes-base/dist/nodes/OpenAi/openai.dark.svg',
|
||||||
|
},
|
||||||
|
supportedNodes: [
|
||||||
|
'n8n-nodes-base.openAi',
|
||||||
|
'@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||||
|
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
'@n8n/n8n-nodes-langchain.lmOpenAi',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
supabaseApi: {
|
||||||
|
name: 'supabaseApi',
|
||||||
|
displayName: 'Supabase API',
|
||||||
|
documentationUrl: 'supabase',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Host',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://your_account.supabase.co',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Service Role Secret',
|
||||||
|
name: 'serviceRole',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authenticate: {
|
||||||
|
type: 'generic',
|
||||||
|
properties: {
|
||||||
|
headers: {
|
||||||
|
apikey: '={{$credentials.serviceRole}}',
|
||||||
|
Authorization: '=Bearer {{$credentials.serviceRole}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
request: {
|
||||||
|
baseURL: '={{$credentials.host}}/rest/v1',
|
||||||
|
headers: {
|
||||||
|
Prefer: 'return=representation',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Supabase/supabase.svg',
|
||||||
|
supportedNodes: ['n8n-nodes-base.supabase'],
|
||||||
|
},
|
||||||
|
slackOAuth2Api: {
|
||||||
|
name: 'slackOAuth2Api',
|
||||||
|
extends: ['oAuth2Api'],
|
||||||
|
displayName: 'Slack OAuth2 API',
|
||||||
|
documentationUrl: 'slack',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Grant Type',
|
||||||
|
name: 'grantType',
|
||||||
|
type: 'hidden',
|
||||||
|
default: 'authorizationCode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Authorization URL',
|
||||||
|
name: 'authUrl',
|
||||||
|
type: 'hidden',
|
||||||
|
default: 'https://slack.com/oauth/v2/authorize',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Access Token URL',
|
||||||
|
name: 'accessTokenUrl',
|
||||||
|
type: 'hidden',
|
||||||
|
default: 'https://slack.com/api/oauth.v2.access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Scope',
|
||||||
|
name: 'scope',
|
||||||
|
type: 'hidden',
|
||||||
|
default: 'chat:write',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Auth URI Query Parameters',
|
||||||
|
name: 'authQueryParameters',
|
||||||
|
type: 'hidden',
|
||||||
|
default:
|
||||||
|
'user_scope=channels:read channels:write chat:write files:read files:write groups:read im:read mpim:read reactions:read reactions:write stars:read stars:write usergroups:write usergroups:read users.profile:read users.profile:write users:read',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Authentication',
|
||||||
|
name: 'authentication',
|
||||||
|
type: 'hidden',
|
||||||
|
default: 'body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'If you get an Invalid Scopes error, make sure you add the correct one <a target="_blank" href="https://docs.n8n.io/integrations/builtin/credentials/slack/#using-oauth">here</a> to your Slack integration',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Slack/slack.svg',
|
||||||
|
supportedNodes: ['n8n-nodes-base.slack'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PERSONAL_OPENAI_CREDENTIAL = TEST_CREDENTIALS[1];
|
||||||
|
export const PROJECT_OPENAI_CREDENTIAL = TEST_CREDENTIALS[4];
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import CredentialPicker from './CredentialPicker.vue';
|
||||||
|
import {
|
||||||
|
PERSONAL_OPENAI_CREDENTIAL,
|
||||||
|
PROJECT_OPENAI_CREDENTIAL,
|
||||||
|
TEST_CREDENTIAL_TYPES,
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
} from './CredentialPicker.test.constants';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { screen } from '@testing-library/vue';
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => {
|
||||||
|
const push = vi.fn();
|
||||||
|
const resolve = vi.fn().mockReturnValue({ href: 'https://test.com' });
|
||||||
|
return {
|
||||||
|
useRouter: () => ({
|
||||||
|
push,
|
||||||
|
resolve,
|
||||||
|
}),
|
||||||
|
useRoute: () => ({}),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CredentialPicker);
|
||||||
|
|
||||||
|
describe('CredentialPicker', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createTestingPinia();
|
||||||
|
credentialsStore = mockedStore(useCredentialsStore);
|
||||||
|
credentialsStore.state.credentials = TEST_CREDENTIALS;
|
||||||
|
credentialsStore.state.credentialTypes = TEST_CREDENTIAL_TYPES;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render', () => {
|
||||||
|
expect(() =>
|
||||||
|
renderComponent({
|
||||||
|
props: {
|
||||||
|
appName: 'OpenAI',
|
||||||
|
credentialType: 'openAiApi',
|
||||||
|
selectedCredentialId: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only render personal credentials of the specified type', async () => {
|
||||||
|
const TEST_APP_NAME = 'OpenAI';
|
||||||
|
const TEST_CREDENTIAL_TYPE = 'openAiApi';
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
appName: TEST_APP_NAME,
|
||||||
|
credentialType: TEST_CREDENTIAL_TYPE,
|
||||||
|
selectedCredentialId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('credential-dropdown')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('credential-dropdown')).toHaveAttribute(
|
||||||
|
'credential-type',
|
||||||
|
TEST_CREDENTIAL_TYPE,
|
||||||
|
);
|
||||||
|
// Open the dropdown
|
||||||
|
await userEvent.click(getByTestId('credential-dropdown'));
|
||||||
|
// Personal openAI credential should be in the dropdown
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(`node-credentials-select-item-${PERSONAL_OPENAI_CREDENTIAL.id}`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
// OpenAI credential that belong to other project should not be in the dropdown
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(`node-credentials-select-item-${PROJECT_OPENAI_CREDENTIAL.id}`),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -26,7 +26,10 @@ const i18n = useI18n();
|
||||||
const wasModalOpenedFromHere = ref(false);
|
const wasModalOpenedFromHere = ref(false);
|
||||||
|
|
||||||
const availableCredentials = computed(() => {
|
const availableCredentials = computed(() => {
|
||||||
return credentialsStore.getCredentialsByType(props.credentialType);
|
const credByType = credentialsStore.getCredentialsByType(props.credentialType);
|
||||||
|
// Only show personal credentials since templates are created in personal by default
|
||||||
|
// Here, we don't care about sharing because credentials cannot be shared with personal project
|
||||||
|
return credByType.filter((credential) => credential.homeProject?.type === 'personal');
|
||||||
});
|
});
|
||||||
|
|
||||||
const credentialOptions = computed(() => {
|
const credentialOptions = computed(() => {
|
||||||
|
@ -98,6 +101,7 @@ listenForModalChanges({
|
||||||
:credential-type="props.credentialType"
|
:credential-type="props.credentialType"
|
||||||
:credential-options="credentialOptions"
|
:credential-options="credentialOptions"
|
||||||
:selected-credential-id="props.selectedCredentialId"
|
:selected-credential-id="props.selectedCredentialId"
|
||||||
|
data-test-id="credential-dropdown"
|
||||||
@credential-selected="onCredentialSelected"
|
@credential-selected="onCredentialSelected"
|
||||||
@new-credential="createNewCredential"
|
@new-credential="createNewCredential"
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue