fix(editor): Fix External secrets typecheck (no-changelog) (#9434)

This commit is contained in:
Alex Grozav 2024-05-17 14:16:00 +03:00 committed by GitHub
parent 28e3e21177
commit db1a40635d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 100 additions and 63 deletions

View file

@ -1803,18 +1803,19 @@ export interface ExternalSecretsProviderSecret {
export type ExternalSecretsProviderData = Record<string, IUpdateInformation['value']>; export type ExternalSecretsProviderData = Record<string, IUpdateInformation['value']>;
export type ExternalSecretsProviderProperty = INodeProperties;
export type ExternalSecretsProviderState = 'connected' | 'tested' | 'initializing' | 'error';
export interface ExternalSecretsProvider { export interface ExternalSecretsProvider {
icon: string; icon: string;
name: string; name: string;
displayName: string; displayName: string;
connected: boolean; connected: boolean;
connectedAt: string | false; connectedAt: string | false;
state: 'connected' | 'tested' | 'initializing' | 'error'; state: ExternalSecretsProviderState;
data?: ExternalSecretsProviderData; data?: ExternalSecretsProviderData;
} properties?: ExternalSecretsProviderProperty[];
export interface ExternalSecretsProviderWithProperties extends ExternalSecretsProvider {
properties: INodeProperties[];
} }
export type CloudUpdateLinkSourceType = export type CloudUpdateLinkSourceType =
@ -1835,6 +1836,7 @@ export type CloudUpdateLinkSourceType =
| 'community-nodes' | 'community-nodes'
| 'workflow-history' | 'workflow-history'
| 'worker-view' | 'worker-view'
| 'external-secrets'
| 'rbac'; | 'rbac';
export type UTMCampaign = export type UTMCampaign =
@ -1855,6 +1857,7 @@ export type UTMCampaign =
| 'upgrade-workflow-history' | 'upgrade-workflow-history'
| 'upgrade-advanced-permissions' | 'upgrade-advanced-permissions'
| 'upgrade-worker-view' | 'upgrade-worker-view'
| 'upgrade-external-secrets'
| 'upgrade-rbac'; | 'upgrade-rbac';
export type N8nBanners = { export type N8nBanners = {

View file

@ -1,8 +1,4 @@
import type { import type { IRestApiContext, ExternalSecretsProvider } from '@/Interface';
IRestApiContext,
ExternalSecretsProvider,
ExternalSecretsProviderWithProperties,
} from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils'; import { makeRestApiRequest } from '@/utils/apiUtils';
export const getExternalSecrets = async ( export const getExternalSecrets = async (
@ -20,7 +16,7 @@ export const getExternalSecretsProviders = async (
export const getExternalSecretsProvider = async ( export const getExternalSecretsProvider = async (
context: IRestApiContext, context: IRestApiContext,
id: string, id: string,
): Promise<ExternalSecretsProviderWithProperties> => { ): Promise<ExternalSecretsProvider> => {
return await makeRestApiRequest(context, 'GET', `/external-secrets/providers/${id}`); return await makeRestApiRequest(context, 'GET', `/external-secrets/providers/${id}`);
}; };

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType, Ref } from 'vue'; import type { PropType } from 'vue';
import type { ExternalSecretsProvider } from '@/Interface'; import type { ExternalSecretsProvider } from '@/Interface';
import ExternalSecretsProviderImage from '@/components/ExternalSecretsProviderImage.ee.vue'; import ExternalSecretsProviderImage from '@/components/ExternalSecretsProviderImage.ee.vue';
import ExternalSecretsProviderConnectionSwitch from '@/components/ExternalSecretsProviderConnectionSwitch.ee.vue'; import ExternalSecretsProviderConnectionSwitch from '@/components/ExternalSecretsProviderConnectionSwitch.ee.vue';
@ -10,7 +10,8 @@ import { useI18n } from '@/composables/useI18n';
import { useExternalSecretsProvider } from '@/composables/useExternalSecretsProvider'; import { useExternalSecretsProvider } from '@/composables/useExternalSecretsProvider';
import { EXTERNAL_SECRETS_PROVIDER_MODAL_KEY } from '@/constants'; import { EXTERNAL_SECRETS_PROVIDER_MODAL_KEY } from '@/constants';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { computed, nextTick, onMounted, toRefs } from 'vue'; import { computed, nextTick, onMounted, toRef } from 'vue';
import { isDateObject } from '@/utils/typeGuards';
const props = defineProps({ const props = defineProps({
provider: { provider: {
@ -24,15 +25,12 @@ const i18n = useI18n();
const uiStore = useUIStore(); const uiStore = useUIStore();
const toast = useToast(); const toast = useToast();
const { provider } = toRefs(props) as Ref<ExternalSecretsProvider>; const provider = toRef(props, 'provider');
const providerData = computed(() => provider.value.data); const providerData = computed(() => provider.value.data ?? {});
const { const { connectionState, testConnection, setConnectionState } = useExternalSecretsProvider(
connectionState, provider,
initialConnectionState, providerData,
normalizedProviderData, );
testConnection,
setConnectionState,
} = useExternalSecretsProvider(provider, providerData);
const actionDropdownOptions = computed(() => [ const actionDropdownOptions = computed(() => [
{ {
@ -50,11 +48,15 @@ const actionDropdownOptions = computed(() => [
]); ]);
const canConnect = computed(() => { const canConnect = computed(() => {
return props.provider.connected || Object.keys(props.provider.data).length > 0; return props.provider.connected || Object.keys(providerData.value).length > 0;
}); });
const formattedDate = computed((provider: ExternalSecretsProvider) => { const formattedDate = computed(() => {
return DateTime.fromISO(props.provider.connectedAt ?? new Date()).toFormat('dd LLL yyyy'); return DateTime.fromISO(
isDateObject(provider.value.connectedAt)
? provider.value.connectedAt.toISOString()
: provider.value.connectedAt || new Date().toISOString(),
).toFormat('dd LLL yyyy');
}); });
onMounted(() => { onMounted(() => {

View file

@ -2,6 +2,7 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { ExternalSecretsProvider } from '@/Interface'; import type { ExternalSecretsProvider } from '@/Interface';
import { computed } from 'vue'; import { computed } from 'vue';
import infisical from '../assets/images/infisical.webp'; import infisical from '../assets/images/infisical.webp';
import doppler from '../assets/images/doppler.webp'; import doppler from '../assets/images/doppler.webp';
import vault from '../assets/images/hashicorp.webp'; import vault from '../assets/images/hashicorp.webp';

View file

@ -2,7 +2,7 @@
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import { EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, MODAL_CONFIRM } from '@/constants'; import { EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, MODAL_CONFIRM } from '@/constants';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import type { PropType, Ref } from 'vue'; import type { PropType } from 'vue';
import type { EventBus } from 'n8n-design-system/utils'; import type { EventBus } from 'n8n-design-system/utils';
import { useExternalSecretsProvider } from '@/composables/useExternalSecretsProvider'; import { useExternalSecretsProvider } from '@/composables/useExternalSecretsProvider';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
@ -10,7 +10,6 @@ import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useRoute } from 'vue-router';
import ParameterInputExpanded from '@/components/ParameterInputExpanded.vue'; import ParameterInputExpanded from '@/components/ParameterInputExpanded.vue';
import type { import type {
IUpdateInformation, IUpdateInformation,
@ -29,7 +28,7 @@ const props = defineProps({
}, },
}); });
const defaultProviderData = { const defaultProviderData: Record<string, Partial<ExternalSecretsProviderData>> = {
infisical: { infisical: {
siteURL: 'https://app.infisical.com', siteURL: 'https://app.infisical.com',
}, },
@ -39,7 +38,6 @@ const externalSecretsStore = useExternalSecretsStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const toast = useToast(); const toast = useToast();
const i18n = useI18n(); const i18n = useI18n();
const route = useRoute();
const { confirm } = useMessage(); const { confirm } = useMessage();
const saving = ref(false); const saving = ref(false);
@ -50,7 +48,7 @@ const labelSize: IParameterLabel = { size: 'medium' };
const provider = computed<ExternalSecretsProvider | undefined>(() => const provider = computed<ExternalSecretsProvider | undefined>(() =>
externalSecretsStore.providers.find((p) => p.name === props.data.name), externalSecretsStore.providers.find((p) => p.name === props.data.name),
) as Ref<ExternalSecretsProvider>; );
const providerData = ref<ExternalSecretsProviderData>({}); const providerData = ref<ExternalSecretsProviderData>({});
const { const {
connectionState, connectionState,
@ -64,7 +62,7 @@ const {
const providerDataUpdated = computed(() => { const providerDataUpdated = computed(() => {
return Object.keys(providerData.value).find((key) => { return Object.keys(providerData.value).find((key) => {
const value = providerData.value[key]; const value = providerData.value[key];
const originalValue = provider.value.data[key]; const originalValue = provider.value?.data?.[key];
return value !== originalValue; return value !== originalValue;
}); });
@ -72,7 +70,7 @@ const providerDataUpdated = computed(() => {
const canSave = computed( const canSave = computed(
() => () =>
provider.value.properties provider.value?.properties
?.filter((property) => property.required && shouldDisplayProperty(property)) ?.filter((property) => property.required && shouldDisplayProperty(property))
.every((property) => { .every((property) => {
const value = providerData.value[property.name]; const value = providerData.value[property.name];
@ -82,21 +80,22 @@ const canSave = computed(
onMounted(async () => { onMounted(async () => {
try { try {
const provider = await externalSecretsStore.getProvider(props.data.name); const fetchedProvider = await externalSecretsStore.getProvider(props.data.name);
providerData.value = { providerData.value = {
...(defaultProviderData[props.data.name] || {}), ...(defaultProviderData[props.data.name] || {}),
...provider.data, ...fetchedProvider.data,
}; };
setConnectionState(provider.state); setConnectionState(fetchedProvider.state);
if (provider.connected) { if (fetchedProvider.connected) {
initialConnectionState.value = provider.state; initialConnectionState.value = fetchedProvider.state;
} else if (Object.keys(provider.data).length) { } else if (Object.keys(fetchedProvider.data ?? {}).length) {
await testConnection(); await testConnection();
} }
if (provider.state === 'connected') { if (fetchedProvider.state === 'connected') {
void externalSecretsStore.reloadProvider(props.data.name); void externalSecretsStore.reloadProvider(props.data.name);
} }
} catch (error) { } catch (error) {
@ -116,6 +115,10 @@ function onValueChange(updateInformation: IUpdateInformation) {
} }
async function save() { async function save() {
if (!provider.value) {
return;
}
try { try {
saving.value = true; saving.value = true;
await externalSecretsStore.updateProvider(provider.value.name, { await externalSecretsStore.updateProvider(provider.value.name, {
@ -143,7 +146,7 @@ async function onBeforeClose() {
const confirmModal = await confirm( const confirmModal = await confirm(
i18n.baseText('settings.externalSecrets.provider.closeWithoutSaving.description', { i18n.baseText('settings.externalSecrets.provider.closeWithoutSaving.description', {
interpolate: { interpolate: {
provider: provider.value.displayName, provider: provider.value?.displayName ?? '',
}, },
}), }),
{ {
@ -162,19 +165,23 @@ async function onBeforeClose() {
return true; return true;
} }
async function onConnectionStateChange() {
await testConnection();
}
</script> </script>
<template> <template>
<Modal <Modal
id="external-secrets-provider-modal" id="external-secrets-provider-modal"
width="812px" width="812px"
:title="provider.displayName" :title="provider?.displayName"
:event-bus="data.eventBus" :event-bus="data.eventBus"
:name="EXTERNAL_SECRETS_PROVIDER_MODAL_KEY" :name="EXTERNAL_SECRETS_PROVIDER_MODAL_KEY"
:before-close="onBeforeClose" :before-close="onBeforeClose"
> >
<template #header> <template #header>
<div :class="$style.header"> <div v-if="provider" :class="$style.header">
<div :class="$style.providerTitle"> <div :class="$style.providerTitle">
<ExternalSecretsProviderImage :provider="provider" class="mr-xs" /> <ExternalSecretsProviderImage :provider="provider" class="mr-xs" />
<span>{{ provider.displayName }}</span> <span>{{ provider.displayName }}</span>
@ -188,7 +195,7 @@ async function onBeforeClose() {
" "
:event-bus="eventBus" :event-bus="eventBus"
:provider="provider" :provider="provider"
@change="testConnection" @change="onConnectionStateChange"
/> />
<n8n-button <n8n-button
type="primary" type="primary"
@ -207,7 +214,7 @@ async function onBeforeClose() {
</template> </template>
<template #content> <template #content>
<div :class="$style.container"> <div v-if="provider" :class="$style.container">
<hr class="mb-l" /> <hr class="mb-l" />
<div v-if="connectionState !== 'initializing'" class="mb-l"> <div v-if="connectionState !== 'initializing'" class="mb-l">
<n8n-callout <n8n-callout

View file

@ -1,36 +1,41 @@
import type { import type {
ExternalSecretsProviderWithProperties,
ExternalSecretsProvider, ExternalSecretsProvider,
IUpdateInformation, IUpdateInformation,
ExternalSecretsProviderData, ExternalSecretsProviderData,
ExternalSecretsProviderProperty,
ExternalSecretsProviderState,
} from '@/Interface'; } from '@/Interface';
import type { Ref } from 'vue'; import type { ComputedRef, Ref } from 'vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
export function useExternalSecretsProvider( export function useExternalSecretsProvider(
provider: Ref<ExternalSecretsProvider>, provider:
| Ref<ExternalSecretsProvider | undefined>
| ComputedRef<ExternalSecretsProvider | undefined>,
providerData: Ref<ExternalSecretsProviderData>, providerData: Ref<ExternalSecretsProviderData>,
) { ) {
const toast = useToast(); const toast = useToast();
const externalSecretsStore = useExternalSecretsStore(); const externalSecretsStore = useExternalSecretsStore();
const initialConnectionState = ref<ExternalSecretsProviderWithProperties['state'] | undefined>( const initialConnectionState = ref<ExternalSecretsProvider['state'] | undefined>('initializing');
'initializing',
);
const connectionState = computed( const connectionState = computed(
() => externalSecretsStore.connectionState[provider.value?.name], () => externalSecretsStore.connectionState[provider.value?.name ?? ''],
); );
const setConnectionState = (state: ExternalSecretsProviderWithProperties['state']) => { const setConnectionState = (state: ExternalSecretsProvider['state']) => {
externalSecretsStore.setConnectionState(provider.value?.name, state); if (!provider.value) {
return;
}
externalSecretsStore.setConnectionState(provider.value.name, state);
}; };
const normalizedProviderData = computed(() => { const normalizedProviderData = computed(() => {
return Object.entries(providerData.value).reduce( return Object.entries(providerData.value).reduce(
(acc, [key, value]) => { (acc, [key, value]) => {
const property = provider.value?.properties?.find((property) => property.name === key); const property = provider.value?.properties?.find((p) => p.name === key);
if (shouldDisplayProperty(property)) { if (property && shouldDisplayProperty(property)) {
acc[key] = value; acc[key] = value;
} }
@ -40,16 +45,14 @@ export function useExternalSecretsProvider(
); );
}); });
function shouldDisplayProperty( function shouldDisplayProperty(property: ExternalSecretsProviderProperty): boolean {
property: ExternalSecretsProviderWithProperties['properties'][0],
): boolean {
let visible = true; let visible = true;
if (property.displayOptions?.show) { if (property.displayOptions?.show) {
visible = visible =
visible && visible &&
Object.entries(property.displayOptions.show).every(([key, value]) => { Object.entries(property.displayOptions.show).every(([key, value]) => {
return value?.includes(providerData.value[key]); return value?.includes(providerData.value[key] as string);
}); });
} }
@ -57,14 +60,20 @@ export function useExternalSecretsProvider(
visible = visible =
visible && visible &&
!Object.entries(property.displayOptions.hide).every(([key, value]) => { !Object.entries(property.displayOptions.hide).every(([key, value]) => {
return value?.includes(providerData.value[key]); return value?.includes(providerData.value[key] as string);
}); });
} }
return visible; return visible;
} }
async function testConnection(options: { showError?: boolean } = { showError: true }) { async function testConnection(
options: { showError?: boolean } = { showError: true },
): Promise<ExternalSecretsProviderState> {
if (!provider.value) {
return 'initializing';
}
try { try {
const { testState } = await externalSecretsStore.testProviderConnection( const { testState } = await externalSecretsStore.testProviderConnection(
provider.value.name, provider.value.name,

View file

@ -35,3 +35,10 @@ declare global {
findLast(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T; findLast(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T;
} }
} }
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.webp';

View file

@ -43,3 +43,9 @@ export const isResourceMapperValue = (value: unknown): value is string | number
export const isJSPlumbEndpointElement = (element: Node): element is HTMLElement => { export const isJSPlumbEndpointElement = (element: Node): element is HTMLElement => {
return 'jtk' in element && 'endpoint' in (element.jtk as object); return 'jtk' in element && 'endpoint' in (element.jtk as object);
}; };
export function isDateObject(date: unknown): date is Date {
return (
!!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as number)
);
}

View file

@ -11,7 +11,13 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"baseUrl": ".", "baseUrl": ".",
"types": ["vitest/globals", "../workflow/src/types.d.ts"], "types": [
"vitest/globals",
"src/shims.d.ts",
"src/shims-vue.d.ts",
"src/v3-infinite-loading.d.ts",
"../workflow/src/types.d.ts"
],
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["src/*"],
"n8n-design-system/*": ["../design-system/src/*"], "n8n-design-system/*": ["../design-system/src/*"],
@ -23,5 +29,5 @@
"useUnknownInCatchVariables": false, "useUnknownInCatchVariables": false,
"experimentalDecorators": true "experimentalDecorators": true
}, },
"include": ["src/**/*.ts", "src/**/*.vue"] "include": [ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
} }