mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
335 lines
8.4 KiB
Vue
335 lines
8.4 KiB
Vue
<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 { EventBus } from 'n8n-design-system/utils';
|
|
import { useExternalSecretsProvider } from '@/composables/useExternalSecretsProvider';
|
|
import { useI18n } from '@/composables/useI18n';
|
|
import { useMessage } from '@/composables/useMessage';
|
|
import { useToast } from '@/composables/useToast';
|
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
|
import ParameterInputExpanded from '@/components/ParameterInputExpanded.vue';
|
|
import type {
|
|
IUpdateInformation,
|
|
ExternalSecretsProviderData,
|
|
ExternalSecretsProvider,
|
|
} 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';
|
|
|
|
const props = defineProps<{
|
|
data: { eventBus: EventBus; name: string };
|
|
}>();
|
|
|
|
const defaultProviderData: Record<string, Partial<ExternalSecretsProviderData>> = {
|
|
infisical: {
|
|
siteURL: 'https://app.infisical.com',
|
|
},
|
|
};
|
|
|
|
const externalSecretsStore = useExternalSecretsStore();
|
|
const toast = useToast();
|
|
const i18n = useI18n();
|
|
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),
|
|
);
|
|
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 fetchedProvider = await externalSecretsStore.getProvider(props.data.name);
|
|
|
|
providerData.value = {
|
|
...(defaultProviderData[props.data.name] || {}),
|
|
...fetchedProvider.data,
|
|
};
|
|
|
|
setConnectionState(fetchedProvider.state);
|
|
|
|
if (fetchedProvider.connected) {
|
|
initialConnectionState.value = fetchedProvider.state;
|
|
} else if (Object.keys(fetchedProvider.data ?? {}).length) {
|
|
await testConnection();
|
|
}
|
|
|
|
if (fetchedProvider.state === 'connected') {
|
|
void externalSecretsStore.reloadProvider(props.data.name);
|
|
}
|
|
} catch (error) {
|
|
toast.showError(error, 'Error');
|
|
}
|
|
});
|
|
|
|
function onValueChange(updateInformation: IUpdateInformation) {
|
|
providerData.value = {
|
|
...providerData.value,
|
|
[updateInformation.name]: updateInformation.value,
|
|
};
|
|
}
|
|
|
|
async function save() {
|
|
if (!provider.value) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async function onConnectionStateChange() {
|
|
await testConnection();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Modal
|
|
id="external-secrets-provider-modal"
|
|
width="812px"
|
|
:title="provider?.displayName"
|
|
:event-bus="data.eventBus"
|
|
:name="EXTERNAL_SECRETS_PROVIDER_MODAL_KEY"
|
|
:before-close="onBeforeClose"
|
|
>
|
|
<template #header>
|
|
<div v-if="provider" :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="onConnectionStateChange"
|
|
/>
|
|
<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 v-if="provider" :class="$style.container">
|
|
<hr class="mb-l" />
|
|
<div v-if="connectionState !== 'initializing'" class="mb-l">
|
|
<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" />
|
|
<ParameterInputExpanded
|
|
v-else
|
|
class="mb-l"
|
|
:parameter="property"
|
|
:value="providerData[property.name]"
|
|
:label="labelSize"
|
|
event-source="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>
|