mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
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:
@@ -0,0 +1,331 @@
|
||||
<script lang="ts" setup>
|
||||
import Modal from './Modal.vue';
|
||||
import { EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, MODAL_CONFIRM } from '@/constants';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import type { PropType, Ref } from 'vue';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { useExternalSecretsProvider, useI18n, useMessage, useToast } from '@/composables';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { useUIStore } from '@/stores';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ParameterInputExpanded from '@/components/ParameterInputExpanded.vue';
|
||||
import type { IUpdateInformation, ExternalSecretsProviderData } from '@/Interface';
|
||||
import type { IParameterLabel } from 'n8n-workflow';
|
||||
import ExternalSecretsProviderImage from '@/components/ExternalSecretsProviderImage.ee.vue';
|
||||
import ExternalSecretsProviderConnectionSwitch from '@/components/ExternalSecretsProviderConnectionSwitch.ee.vue';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import type { ExternalSecretsProvider } from '@/Interface';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<{ eventBus: EventBus; name: string }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const defaultProviderData = {
|
||||
infisical: {
|
||||
siteURL: 'https://app.infisical.com',
|
||||
},
|
||||
};
|
||||
|
||||
const externalSecretsStore = useExternalSecretsStore();
|
||||
const uiStore = useUIStore();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
const { confirm } = useMessage();
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
const eventBus = createEventBus();
|
||||
|
||||
const labelSize: IParameterLabel = { size: 'medium' };
|
||||
|
||||
const provider = computed<ExternalSecretsProvider | undefined>(() =>
|
||||
externalSecretsStore.providers.find((p) => p.name === props.data.name),
|
||||
) as Ref<ExternalSecretsProvider>;
|
||||
const providerData = ref<ExternalSecretsProviderData>({});
|
||||
const {
|
||||
connectionState,
|
||||
initialConnectionState,
|
||||
normalizedProviderData,
|
||||
shouldDisplayProperty,
|
||||
setConnectionState,
|
||||
testConnection,
|
||||
} = useExternalSecretsProvider(provider, providerData);
|
||||
|
||||
const providerDataUpdated = computed(() => {
|
||||
return Object.keys(providerData.value).find((key) => {
|
||||
const value = providerData.value[key];
|
||||
const originalValue = provider.value.data[key];
|
||||
|
||||
return value !== originalValue;
|
||||
});
|
||||
});
|
||||
|
||||
const canSave = computed(
|
||||
() =>
|
||||
provider.value.properties
|
||||
?.filter((property) => property.required && shouldDisplayProperty(property))
|
||||
.every((property) => {
|
||||
const value = providerData.value[property.name];
|
||||
return !!value;
|
||||
}) && providerDataUpdated.value,
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const provider = await externalSecretsStore.getProvider(props.data.name);
|
||||
providerData.value = {
|
||||
...(defaultProviderData[props.data.name] || {}),
|
||||
...provider.data,
|
||||
};
|
||||
|
||||
setConnectionState(provider.state);
|
||||
|
||||
if (provider.connected) {
|
||||
initialConnectionState.value = provider.state;
|
||||
} else if (Object.keys(provider.data).length) {
|
||||
await testConnection();
|
||||
}
|
||||
|
||||
if (provider.state === 'connected') {
|
||||
void externalSecretsStore.reloadProvider(props.data.name);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Error');
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
uiStore.closeModal(EXTERNAL_SECRETS_PROVIDER_MODAL_KEY);
|
||||
}
|
||||
|
||||
function onValueChange(updateInformation: IUpdateInformation) {
|
||||
providerData.value = {
|
||||
...providerData.value,
|
||||
[updateInformation.name]: updateInformation.value,
|
||||
};
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
saving.value = true;
|
||||
await externalSecretsStore.updateProvider(provider.value.name, {
|
||||
data: normalizedProviderData.value,
|
||||
});
|
||||
|
||||
setConnectionState(provider.value.state);
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Error');
|
||||
}
|
||||
|
||||
await testConnection();
|
||||
|
||||
if (initialConnectionState.value === 'initializing' && connectionState.value === 'tested') {
|
||||
setTimeout(() => {
|
||||
eventBus.emit('connect', true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
async function onBeforeClose() {
|
||||
if (providerDataUpdated.value) {
|
||||
const confirmModal = await confirm(
|
||||
i18n.baseText('settings.externalSecrets.provider.closeWithoutSaving.description', {
|
||||
interpolate: {
|
||||
provider: provider.value.displayName,
|
||||
},
|
||||
}),
|
||||
{
|
||||
title: i18n.baseText('settings.externalSecrets.provider.closeWithoutSaving.title'),
|
||||
confirmButtonText: i18n.baseText(
|
||||
'settings.externalSecrets.provider.closeWithoutSaving.confirm',
|
||||
),
|
||||
cancelButtonText: i18n.baseText(
|
||||
'settings.externalSecrets.provider.closeWithoutSaving.cancel',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return confirmModal !== MODAL_CONFIRM;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
id="external-secrets-provider-modal"
|
||||
width="812px"
|
||||
:title="provider.displayName"
|
||||
:eventBus="data.eventBus"
|
||||
:name="EXTERNAL_SECRETS_PROVIDER_MODAL_KEY"
|
||||
:before-close="onBeforeClose"
|
||||
>
|
||||
<template #header>
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.providerTitle">
|
||||
<ExternalSecretsProviderImage :provider="provider" class="mr-xs" />
|
||||
<span>{{ provider.displayName }}</span>
|
||||
</div>
|
||||
<div :class="$style.providerActions">
|
||||
<ExternalSecretsProviderConnectionSwitch
|
||||
class="mr-s"
|
||||
:disabled="
|
||||
(connectionState === 'initializing' || connectionState === 'error') &&
|
||||
!provider.connected
|
||||
"
|
||||
:event-bus="eventBus"
|
||||
:provider="provider"
|
||||
@change="testConnection"
|
||||
/>
|
||||
<n8n-button
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="!canSave && !saving"
|
||||
@click="save"
|
||||
>
|
||||
{{
|
||||
i18n.baseText(
|
||||
`settings.externalSecrets.provider.buttons.${saving ? 'saving' : 'save'}`,
|
||||
)
|
||||
}}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div :class="$style.container">
|
||||
<hr class="mb-l" />
|
||||
<div class="mb-l" v-if="connectionState !== 'initializing'">
|
||||
<n8n-callout
|
||||
v-if="connectionState === 'connected' || connectionState === 'tested'"
|
||||
theme="success"
|
||||
>
|
||||
{{
|
||||
i18n.baseText(
|
||||
`settings.externalSecrets.provider.testConnection.success${
|
||||
provider.connected ? '.connected' : ''
|
||||
}`,
|
||||
{
|
||||
interpolate: {
|
||||
count: `${externalSecretsStore.secrets[provider.name]?.length}`,
|
||||
provider: provider.displayName,
|
||||
},
|
||||
},
|
||||
)
|
||||
}}
|
||||
<span v-if="provider.connected">
|
||||
<br />
|
||||
<i18n-t
|
||||
keypath="settings.externalSecrets.provider.testConnection.success.connected.usage"
|
||||
>
|
||||
<template #code>
|
||||
<code>{{ `\{\{ \$secrets\.${provider.name}\.secret_name \}\}` }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<n8n-link :href="i18n.baseText('settings.externalSecrets.docs.use')" size="small">
|
||||
{{
|
||||
i18n.baseText(
|
||||
'settings.externalSecrets.provider.testConnection.success.connected.docs',
|
||||
)
|
||||
}}
|
||||
</n8n-link>
|
||||
</span>
|
||||
</n8n-callout>
|
||||
<n8n-callout v-else-if="connectionState === 'error'" theme="danger">
|
||||
{{
|
||||
i18n.baseText(
|
||||
`settings.externalSecrets.provider.testConnection.error${
|
||||
provider.connected ? '.connected' : ''
|
||||
}`,
|
||||
{
|
||||
interpolate: { provider: provider.displayName },
|
||||
},
|
||||
)
|
||||
}}
|
||||
</n8n-callout>
|
||||
</div>
|
||||
|
||||
<form
|
||||
v-for="property in provider.properties"
|
||||
v-show="shouldDisplayProperty(property)"
|
||||
:key="property.name"
|
||||
autocomplete="off"
|
||||
data-test-id="external-secrets-provider-properties-form"
|
||||
@submit.prevent
|
||||
>
|
||||
<n8n-notice v-if="property.type === 'notice'" :content="property.displayName" />
|
||||
<parameter-input-expanded
|
||||
v-else
|
||||
class="mb-l"
|
||||
:parameter="property"
|
||||
:value="providerData[property.name]"
|
||||
:label="labelSize"
|
||||
eventSource="external-secrets-provider"
|
||||
@update="onValueChange"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
> * {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.providerTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.providerActions {
|
||||
flex: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
#external-secrets-provider-modal {
|
||||
.el-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
position: relative;
|
||||
top: unset;
|
||||
right: unset;
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user