refactor(editor): Convert credential related components to composition API (no-changelog) (#10530)

This commit is contained in:
Elias Meire 2024-08-29 16:30:19 +02:00 committed by GitHub
parent 405c55a1f7
commit 402a8b40c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 763 additions and 858 deletions

View file

@ -1130,10 +1130,7 @@ function resetCredentialData(): void {
/>
</div>
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
<CredentialInfo
:current-credential="currentCredential"
:credential-permissions="credentialPermissions"
/>
<CredentialInfo :current-credential="currentCredential" />
</div>
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>

View file

@ -1,57 +1,52 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import TimeAgo from '../TimeAgo.vue';
import type { INodeTypeDescription } from 'n8n-workflow';
import { useI18n } from '@/composables/useI18n';
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
import { N8nText } from 'n8n-design-system';
export default defineComponent({
name: 'CredentialInfo',
components: {
TimeAgo,
},
props: ['currentCredential', 'credentialPermissions'],
methods: {
shortNodeType(nodeType: INodeTypeDescription) {
return this.$locale.shortNodeType(nodeType.name);
},
},
});
type Props = {
currentCredential: ICredentialsResponse | ICredentialsDecryptedResponse | null;
};
defineProps<Props>();
const i18n = useI18n();
</script>
<template>
<div :class="$style.container">
<el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.created') }}
</n8n-text>
<N8nText :compact="true" :bold="true">
{{ i18n.baseText('credentialEdit.credentialInfo.created') }}
</N8nText>
</el-col>
<el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"
<N8nText :compact="true"
><TimeAgo :date="currentCredential.createdAt" :capitalize="true"
/></n8n-text>
/></N8nText>
</el-col>
</el-row>
<el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.lastModified') }}
</n8n-text>
<N8nText :compact="true" :bold="true">
{{ i18n.baseText('credentialEdit.credentialInfo.lastModified') }}
</N8nText>
</el-col>
<el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"
<N8nText :compact="true"
><TimeAgo :date="currentCredential.updatedAt" :capitalize="true"
/></n8n-text>
/></N8nText>
</el-col>
</el-row>
<el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.id') }}
</n8n-text>
<N8nText :compact="true" :bold="true">
{{ i18n.baseText('credentialEdit.credentialInfo.id') }}
</N8nText>
</el-col>
<el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text>
<N8nText :compact="true">{{ currentCredential.id }}</N8nText>
</el-col>
</el-row>
</div>

View file

@ -1,162 +1,114 @@
<script lang="ts">
import type {
ICredentialsResponse,
ICredentialsDecryptedResponse,
IUserListAction,
} from '@/Interface';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { useMessage } from '@/composables/useMessage';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
<script setup lang="ts">
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useI18n } from '@/composables/useI18n';
import { EnterpriseEditionFeature } from '@/constants';
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
import type { PermissionsRecord } from '@/permissions';
import { useProjectsStore } from '@/stores/projects.store';
import { useRolesStore } from '@/stores/roles.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useUsageStore } from '@/stores/usage.store';
import { EnterpriseEditionFeature } from '@/constants';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useProjectsStore } from '@/stores/projects.store';
import { useUsersStore } from '@/stores/users.store';
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { PermissionsRecord } from '@/permissions';
import type { EventBus } from 'n8n-design-system/utils';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
import { splitName } from '@/utils/projects.utils';
import type { EventBus } from 'n8n-design-system/utils';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { computed, onMounted, ref, watch } from 'vue';
export default defineComponent({
name: 'CredentialSharing',
components: {
ProjectSharing,
},
props: {
credential: {
type: Object as PropType<ICredentialsResponse | ICredentialsDecryptedResponse | null>,
default: null,
},
credentialId: {
type: String,
required: true,
},
credentialData: {
type: Object as PropType<ICredentialDataDecryptedObject>,
required: true,
},
credentialPermissions: {
type: Object as PropType<PermissionsRecord['credential']>,
required: true,
},
modalBus: {
type: Object as PropType<EventBus>,
required: true,
},
},
emits: ['update:modelValue'],
setup() {
return {
...useMessage(),
};
},
data() {
return {
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
};
},
computed: {
...mapStores(
useCredentialsStore,
useUsersStore,
useUsageStore,
useUIStore,
useSettingsStore,
useProjectsStore,
useRolesStore,
),
usersListActions(): IUserListAction[] {
return [
{
label: this.$locale.baseText('credentialEdit.credentialSharing.list.delete'),
value: 'delete',
},
];
},
isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
},
credentialOwnerName(): string {
const { firstName, lastName, email } = splitName(this.credential?.homeProject?.name ?? '');
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
},
credentialDataHomeProject(): ProjectSharingData | undefined {
const credentialContainsProjectSharingData = (
data: ICredentialDataDecryptedObject,
): data is { homeProject: ProjectSharingData } => {
return 'homeProject' in data;
};
type Props = {
credentialId: string;
credentialData: ICredentialDataDecryptedObject;
credentialPermissions: PermissionsRecord['credential'];
credential?: ICredentialsResponse | ICredentialsDecryptedResponse | null;
modalBus: EventBus;
};
return this.credentialData && credentialContainsProjectSharingData(this.credentialData)
? this.credentialData.homeProject
: undefined;
},
isCredentialSharedWithCurrentUser(): boolean {
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
const props = withDefaults(defineProps<Props>(), { credential: null });
return this.credentialData.sharedWithProjects.some((sharee) => {
return typeof sharee === 'object' && 'id' in sharee
? sharee.id === this.usersStore.currentUser?.id
: false;
});
},
projects(): ProjectListItem[] {
return this.projectsStore.projects.filter(
(project) =>
project.id !== this.credential?.homeProject?.id &&
project.id !== this.credentialDataHomeProject?.id,
);
},
homeProject(): ProjectSharingData | undefined {
return this.credential?.homeProject ?? this.credentialDataHomeProject;
},
isHomeTeamProject(): boolean {
return this.homeProject?.type === ProjectTypes.Team;
},
credentialRoleTranslations(): Record<string, string> {
return {
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
};
},
credentialRoles(): RoleMap['credential'] {
return this.rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
role,
name: this.credentialRoleTranslations[role],
scopes,
licensed,
}));
},
sharingSelectPlaceholder() {
return this.projectsStore.teamProjects.length
? this.$locale.baseText('projects.sharing.select.placeholder.project')
: this.$locale.baseText('projects.sharing.select.placeholder.user');
},
},
watch: {
sharedWithProjects: {
handler(changedSharedWithProjects: ProjectSharingData[]) {
this.$emit('update:modelValue', changedSharedWithProjects);
},
deep: true,
},
},
async mounted() {
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
},
methods: {
goToUpgrade() {
void this.uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
},
},
const emit = defineEmits<{
'update:modelValue': [value: ProjectSharingData[]];
}>();
const i18n = useI18n();
const usersStore = useUsersStore();
const uiStore = useUIStore();
const settingsStore = useSettingsStore();
const projectsStore = useProjectsStore();
const rolesStore = useRolesStore();
const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
const isSharingEnabled = computed(
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
);
const credentialOwnerName = computed(() => {
const { firstName, lastName, email } = splitName(props.credential?.homeProject?.name ?? '');
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
});
const credentialDataHomeProject = computed<ProjectSharingData | undefined>(() => {
const credentialContainsProjectSharingData = (
data: ICredentialDataDecryptedObject,
): data is { homeProject: ProjectSharingData } => {
return 'homeProject' in data;
};
return props.credentialData && credentialContainsProjectSharingData(props.credentialData)
? props.credentialData.homeProject
: undefined;
});
const projects = computed<ProjectListItem[]>(() => {
return projectsStore.projects.filter(
(project) =>
project.id !== props.credential?.homeProject?.id &&
project.id !== credentialDataHomeProject.value?.id,
);
});
const homeProject = computed<ProjectSharingData | undefined>(
() => props.credential?.homeProject ?? credentialDataHomeProject.value,
);
const isHomeTeamProject = computed(() => homeProject.value?.type === ProjectTypes.Team);
const credentialRoleTranslations = computed<Record<string, string>>(() => {
return {
'credential:user': i18n.baseText('credentialEdit.credentialSharing.role.user'),
};
});
const credentialRoles = computed<RoleMap['credential']>(() => {
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
role,
name: credentialRoleTranslations.value[role],
scopes,
licensed,
}));
});
const sharingSelectPlaceholder = computed(() =>
projectsStore.teamProjects.length
? i18n.baseText('projects.sharing.select.placeholder.project')
: i18n.baseText('projects.sharing.select.placeholder.user'),
);
watch(
sharedWithProjects,
(changedSharedWithProjects) => {
emit('update:modelValue', changedSharedWithProjects);
},
{ deep: true },
);
onMounted(async () => {
await Promise.all([usersStore.fetchUsers(), projectsStore.getAllProjects()]);
});
function goToUpgrade() {
void uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
}
</script>
<template>
@ -164,33 +116,29 @@ export default defineComponent({
<div v-if="!isSharingEnabled">
<N8nActionBox
:heading="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
)
i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title)
"
:description="
$locale.baseText(
i18n.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description,
)
"
:button-text="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button,
)
i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button)
"
@click:button="goToUpgrade"
/>
</div>
<div v-else>
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
{{ i18n.baseText('credentialEdit.credentialSharing.info.owner') }}
</N8nInfoTip>
<N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
{{ i18n.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
</N8nInfoTip>
<N8nInfoTip v-else :bold="false" class="mb-s">
{{
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
i18n.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
interpolate: { credentialOwnerName },
})
}}

View file

@ -1,116 +1,121 @@
<script lang="ts">
import type { ICredentialType } from 'n8n-workflow';
import { defineComponent } from 'vue';
<script setup lang="ts">
import type { ICredentialType, INodeProperties, NodeParameterValue } from 'n8n-workflow';
import { computed, ref } from 'vue';
import ScopesNotice from '@/components/ScopesNotice.vue';
import NodeCredentials from '@/components/NodeCredentials.vue';
import { mapStores } from 'pinia';
import { useCredentialsStore } from '@/stores/credentials.store';
import { N8nOption, N8nSelect } from 'n8n-design-system';
import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
export default defineComponent({
name: 'CredentialsSelect',
components: {
ScopesNotice,
NodeCredentials,
},
props: [
'activeCredentialType',
'node',
'parameter',
'inputSize',
'displayValue',
'isReadOnly',
'displayTitle',
],
emits: ['update:modelValue', 'setFocus', 'onBlur', 'credentialSelected'],
computed: {
...mapStores(useCredentialsStore),
allCredentialTypes(): ICredentialType[] {
return this.credentialsStore.allCredentialTypes;
},
scopes(): string[] {
if (!this.activeCredentialType) return [];
type Props = {
activeCredentialType: string;
parameter: INodeProperties;
node?: INodeUi;
inputSize?: 'small' | 'large' | 'mini' | 'medium' | 'xlarge';
displayValue: NodeParameterValue;
isReadOnly: boolean;
displayTitle: string;
};
return this.credentialsStore.getScopesByCredentialType(this.activeCredentialType);
},
supportedCredentialTypes(): ICredentialType[] {
return this.allCredentialTypes.filter((c: ICredentialType) => this.isSupported(c.name));
},
},
methods: {
focus() {
const selectRef = this.$refs.innerSelect as HTMLElement | undefined;
if (selectRef) {
selectRef.focus();
}
},
/**
* Check if a credential type belongs to one of the supported sets defined
* in the `credentialTypes` key in a `credentialsSelect` parameter
*/
isSupported(name: string): boolean {
const supported = this.getSupportedSets(this.parameter.credentialTypes);
const props = defineProps<Props>();
const checkedCredType = this.credentialsStore.getCredentialTypeByName(name);
if (!checkedCredType) return false;
const emit = defineEmits<{
'update:modelValue': [value: string];
setFocus: [];
onBlur: [];
credentialSelected: [update: INodeUpdatePropertiesInformation];
}>();
for (const property of supported.has) {
if (checkedCredType[property as keyof ICredentialType] !== undefined) {
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
const credentialsStore = useCredentialsStore();
return true;
}
}
const innerSelectRef = ref<HTMLSelectElement>();
if (
checkedCredType.extends &&
checkedCredType.extends.some((parentType: string) => supported.extends.includes(parentType))
) {
return true;
}
const allCredentialTypes = computed(() => credentialsStore.allCredentialTypes);
const scopes = computed(() => {
if (!props.activeCredentialType) return [];
if (checkedCredType.extends && supported.extends.length) {
// recurse upward until base credential type
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
return checkedCredType.extends.reduce(
(acc: boolean, parentType: string) => acc || this.isSupported(parentType),
false,
);
}
return false;
},
getSupportedSets(credentialTypes: string[]) {
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
(acc, cur) => {
const _extends = cur.split('extends:');
if (_extends.length === 2) {
acc.extends.push(_extends[1]);
return acc;
}
const _has = cur.split('has:');
if (_has.length === 2) {
acc.has.push(_has[1]);
return acc;
}
return acc;
},
{ extends: [], has: [] },
);
},
},
return credentialsStore.getScopesByCredentialType(props.activeCredentialType);
});
const supportedCredentialTypes = computed(() => {
return allCredentialTypes.value.filter((c: ICredentialType) => isSupported(c.name));
});
function focus() {
if (innerSelectRef.value) {
innerSelectRef.value.focus();
}
}
/**
* Check if a credential type belongs to one of the supported sets defined
* in the `credentialTypes` key in a `credentialsSelect` parameter
*/
function isSupported(name: string): boolean {
const supported = getSupportedSets(props.parameter.credentialTypes ?? []);
const checkedCredType = credentialsStore.getCredentialTypeByName(name);
if (!checkedCredType) return false;
for (const property of supported.has) {
if (checkedCredType[property as keyof ICredentialType] !== undefined) {
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
return true;
}
}
if (
checkedCredType.extends &&
checkedCredType.extends.some((parentType: string) => supported.extends.includes(parentType))
) {
return true;
}
if (checkedCredType.extends && supported.extends.length) {
// recurse upward until base credential type
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
return checkedCredType.extends.reduce(
(acc: boolean, parentType: string) => acc || isSupported(parentType),
false,
);
}
return false;
}
function getSupportedSets(credentialTypes: string[]) {
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
(acc, cur) => {
const _extends = cur.split('extends:');
if (_extends.length === 2) {
acc.extends.push(_extends[1]);
return acc;
}
const _has = cur.split('has:');
if (_has.length === 2) {
acc.has.push(_has[1]);
return acc;
}
return acc;
},
{ extends: [], has: [] },
);
}
defineExpose({ focus });
</script>
<template>
<div>
<div :class="$style['parameter-value-container']">
<n8n-select
ref="innerSelect"
<N8nSelect
ref="innerSelectRef"
:size="inputSize"
filterable
:model-value="displayValue"
@ -118,12 +123,12 @@ export default defineComponent({
:title="displayTitle"
:disabled="isReadOnly"
data-test-id="credential-select"
@update:model-value="(value: string) => $emit('update:modelValue', value)"
@update:model-value="(value: string) => emit('update:modelValue', value)"
@keydown.stop
@focus="$emit('setFocus')"
@blur="$emit('onBlur')"
@focus="emit('setFocus')"
@blur="emit('onBlur')"
>
<n8n-option
<N8nOption
v-for="credType in supportedCredentialTypes"
:key="credType.name"
:value="credType.name"
@ -135,8 +140,8 @@ export default defineComponent({
{{ credType.displayName }}
</div>
</div>
</n8n-option>
</n8n-select>
</N8nOption>
</N8nSelect>
<slot name="issues-and-options" />
</div>
@ -147,10 +152,11 @@ export default defineComponent({
/>
<div>
<NodeCredentials
v-if="node"
:node="node"
:readonly="isReadOnly"
:override-cred-type="node.parameters[parameter.name]"
@credential-selected="(updateInformation) => $emit('credentialSelected', updateInformation)"
:override-cred-type="node?.parameters[parameter.name]"
@credential-selected="(updateInformation) => emit('credentialSelected', updateInformation)"
/>
</div>
</div>

View file

@ -1,69 +1,59 @@
<script lang="ts">
import { defineComponent } from 'vue';
import Modal from './Modal.vue';
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
import { mapStores } from 'pinia';
<script setup lang="ts">
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { N8nButton, N8nSelect } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { onMounted, ref } from 'vue';
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
import Modal from './Modal.vue';
export default defineComponent({
name: 'CredentialsSelectModal',
components: {
Modal,
},
setup() {
const externalHooks = useExternalHooks();
return {
externalHooks,
};
},
data() {
return {
modalBus: createEventBus(),
selected: '',
loading: true,
CREDENTIAL_SELECT_MODAL_KEY,
};
},
async mounted() {
try {
await this.credentialsStore.fetchCredentialTypes(false);
} catch (e) {}
this.loading = false;
const externalHooks = useExternalHooks();
const telemetry = useTelemetry();
setTimeout(() => {
const elementRef = this.$refs.select as HTMLSelectElement | undefined;
if (elementRef) {
elementRef.focus();
}
}, 0);
},
computed: {
...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore),
},
methods: {
onSelect(type: string) {
this.selected = type;
},
openCredentialType() {
this.modalBus.emit('close');
this.uiStore.openNewCredential(this.selected);
const modalBus = ref(createEventBus());
const selected = ref('');
const loading = ref(true);
const selectRef = ref<HTMLSelectElement>();
const telemetryPayload = {
credential_type: this.selected,
source: 'primary_menu',
new_credential: true,
workflow_id: this.workflowsStore.workflowId,
};
const credentialsStore = useCredentialsStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
this.$telemetry.track('User opened Credential modal', telemetryPayload);
void this.externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
},
},
onMounted(async () => {
try {
await credentialsStore.fetchCredentialTypes(false);
} catch (e) {}
loading.value = false;
setTimeout(() => {
if (selectRef.value) {
selectRef.value.focus();
}
}, 0);
});
function onSelect(type: string) {
selected.value = type;
}
function openCredentialType() {
modalBus.value.emit('close');
uiStore.openNewCredential(selected.value);
const telemetryPayload = {
credential_type: selected.value,
source: 'primary_menu',
new_credential: true,
workflow_id: workflowsStore.workflowId,
};
telemetry.track('User opened Credential modal', telemetryPayload);
void externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
}
</script>
<template>
@ -86,8 +76,8 @@ export default defineComponent({
<div :class="$style.subtitle">
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
</div>
<n8n-select
ref="select"
<N8nSelect
ref="selectRef"
filterable
default-first-option
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
@ -99,7 +89,7 @@ export default defineComponent({
<template #prefix>
<font-awesome-icon icon="search" />
</template>
<n8n-option
<N8nOption
v-for="credential in credentialsStore.allCredentialTypes"
:key="credential.name"
:value="credential.name"
@ -107,12 +97,12 @@ export default defineComponent({
filterable
data-test-id="new-credential-type-select-option"
/>
</n8n-select>
</N8nSelect>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button
<N8nButton
:label="$locale.baseText('credentialSelectModal.continue')"
float="right"
size="large"

File diff suppressed because it is too large Load diff