feat: External Secrets storage for credentials (#6477)
Github issue / Community forum post (link here to close automatically): --------- Co-authored-by: Romain Minaud <romain.minaud@gmail.com> Co-authored-by: Valya Bullions <valya@n8n.io> Co-authored-by: Csaba Tuncsik <csaba@n8n.io> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
@@ -56,6 +56,7 @@ import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useExternalSecretsStore } from '@/stores';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
|
||||
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
|
||||
@@ -84,6 +85,7 @@ export default defineComponent({
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useSourceControlStore,
|
||||
useExternalSecretsStore,
|
||||
),
|
||||
allCredentials(): ICredentialsResponse[] {
|
||||
return this.credentialsStore.allCredentials;
|
||||
@@ -107,6 +109,7 @@ export default defineComponent({
|
||||
const loadPromises = [
|
||||
this.credentialsStore.fetchAllCredentials(),
|
||||
this.credentialsStore.fetchCredentialTypes(false),
|
||||
this.externalSecretsStore.fetchAllSecrets(),
|
||||
];
|
||||
|
||||
if (this.nodeTypesStore.allNodeTypes.length === 0) {
|
||||
|
||||
@@ -288,6 +288,7 @@ import {
|
||||
useSettingsStore,
|
||||
useUIStore,
|
||||
useHistoryStore,
|
||||
useExternalSecretsStore,
|
||||
} from '@/stores';
|
||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||
import { getAccountAge, getConnectionInfo, getNodeViewTab } from '@/utils';
|
||||
@@ -496,6 +497,7 @@ export default defineComponent({
|
||||
useEnvironmentsStore,
|
||||
useWorkflowsEEStore,
|
||||
useHistoryStore,
|
||||
useExternalSecretsStore,
|
||||
),
|
||||
nativelyNumberSuffixedDefaults(): string[] {
|
||||
return this.nodeTypesStore.nativelyNumberSuffixedDefaults;
|
||||
@@ -3603,6 +3605,9 @@ export default defineComponent({
|
||||
async loadVariables(): Promise<void> {
|
||||
await this.environmentsStore.fetchAllVariables();
|
||||
},
|
||||
async loadSecrets(): Promise<void> {
|
||||
await this.externalSecretsStore.fetchAllSecrets();
|
||||
},
|
||||
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
||||
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
|
||||
|
||||
@@ -3909,6 +3914,7 @@ export default defineComponent({
|
||||
this.loadCredentials(),
|
||||
this.loadCredentialTypes(),
|
||||
this.loadVariables(),
|
||||
this.loadSecrets(),
|
||||
];
|
||||
|
||||
if (this.nodeTypesStore.allNodeTypes.length === 0) {
|
||||
|
||||
75
packages/editor-ui/src/views/SettingsExternalSecrets.vue
Normal file
75
packages/editor-ui/src/views/SettingsExternalSecrets.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useI18n, useMessage, useToast } from '@/composables';
|
||||
import { useExternalSecretsStore } from '@/stores';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import ExternalSecretsProviderCard from '@/components/ExternalSecretsProviderCard.ee.vue';
|
||||
import type { ExternalSecretsProvider } from '@/Interface';
|
||||
|
||||
const i18n = useI18n();
|
||||
const uiStore = useUIStore();
|
||||
const externalSecretsStore = useExternalSecretsStore();
|
||||
const message = useMessage();
|
||||
const toast = useToast();
|
||||
|
||||
const sortedProviders = computed(() => {
|
||||
return ([...externalSecretsStore.providers] as ExternalSecretsProvider[]).sort((a, b) => {
|
||||
return b.name.localeCompare(a.name);
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
void externalSecretsStore.fetchAllSecrets();
|
||||
void externalSecretsStore.getProviders();
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('error'));
|
||||
}
|
||||
});
|
||||
|
||||
function goToUpgrade() {
|
||||
uiStore.goToUpgrade('external-secrets', 'upgrade-external-secrets');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-3xl">
|
||||
<n8n-heading size="2xlarge">{{ i18n.baseText('settings.externalSecrets.title') }}</n8n-heading>
|
||||
<div
|
||||
v-if="externalSecretsStore.isEnterpriseExternalSecretsEnabled"
|
||||
data-test-id="external-secrets-content-licensed"
|
||||
>
|
||||
<n8n-callout theme="secondary" class="mt-2xl mb-l">
|
||||
{{ i18n.baseText('settings.externalSecrets.info') }}
|
||||
<a :href="i18n.baseText('settings.externalSecrets.docs')" target="_blank">
|
||||
{{ i18n.baseText('settings.externalSecrets.info.link') }}
|
||||
</a>
|
||||
</n8n-callout>
|
||||
<ExternalSecretsProviderCard
|
||||
v-for="provider in sortedProviders"
|
||||
:key="provider.name"
|
||||
:provider="provider"
|
||||
/>
|
||||
</div>
|
||||
<n8n-action-box
|
||||
v-else
|
||||
class="mt-2xl mb-l"
|
||||
data-test-id="external-secrets-content-unlicensed"
|
||||
:buttonText="i18n.baseText('settings.externalSecrets.actionBox.buttonText')"
|
||||
@click="goToUpgrade"
|
||||
>
|
||||
<template #heading>
|
||||
<span>{{ i18n.baseText('settings.externalSecrets.actionBox.title') }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<i18n-t keypath="settings.externalSecrets.actionBox.description">
|
||||
<template #link>
|
||||
<a :href="i18n.baseText('settings.externalSecrets.docs')" target="_blank">
|
||||
{{ i18n.baseText('settings.externalSecrets.actionBox.description.link') }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
</n8n-action-box>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { merge } from 'lodash-es';
|
||||
import { EnterpriseEditionFeature, STORES } from '@/constants';
|
||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import SettingsExternalSecrets from '@/views/SettingsExternalSecrets.vue';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useSettingsStore } from '@/stores';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let externalSecretsStore: ReturnType<typeof useExternalSecretsStore>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
|
||||
const renderComponent = createComponentRenderer(SettingsExternalSecrets);
|
||||
|
||||
describe('SettingsExternalSecrets', () => {
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
|
||||
},
|
||||
},
|
||||
});
|
||||
externalSecretsStore = useExternalSecretsStore(pinia);
|
||||
settingsStore = useSettingsStore();
|
||||
|
||||
await settingsStore.getSettings();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('should render paywall state when there is no license', () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent({ pinia });
|
||||
|
||||
expect(queryByTestId('external-secrets-content-licensed')).not.toBeInTheDocument();
|
||||
expect(getByTestId('external-secrets-content-unlicensed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render licensed content', () => {
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.ExternalSecrets] = true;
|
||||
|
||||
const { getByTestId, queryByTestId } = renderComponent({ pinia });
|
||||
|
||||
expect(getByTestId('external-secrets-content-licensed')).toBeInTheDocument();
|
||||
expect(queryByTestId('external-secrets-content-unlicensed')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user