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(),
type Props = {
credentialId: string;
credentialData: ICredentialDataDecryptedObject;
credentialPermissions: PermissionsRecord['credential'];
credential?: ICredentialsResponse | ICredentialsDecryptedResponse | null;
modalBus: EventBus;
};
},
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 ?? '');
const props = withDefaults(defineProps<Props>(), { credential: null });
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 ?? '';
},
credentialDataHomeProject(): ProjectSharingData | undefined {
});
const credentialDataHomeProject = computed<ProjectSharingData | undefined>(() => {
const credentialContainsProjectSharingData = (
data: ICredentialDataDecryptedObject,
): data is { homeProject: ProjectSharingData } => {
return 'homeProject' in data;
};
return this.credentialData && credentialContainsProjectSharingData(this.credentialData)
? this.credentialData.homeProject
return props.credentialData && credentialContainsProjectSharingData(props.credentialData)
? props.credentialData.homeProject
: undefined;
},
isCredentialSharedWithCurrentUser(): boolean {
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
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(
const projects = computed<ProjectListItem[]>(() => {
return projectsStore.projects.filter(
(project) =>
project.id !== this.credential?.homeProject?.id &&
project.id !== this.credentialDataHomeProject?.id,
project.id !== props.credential?.homeProject?.id &&
project.id !== credentialDataHomeProject.value?.id,
);
},
homeProject(): ProjectSharingData | undefined {
return this.credential?.homeProject ?? this.credentialDataHomeProject;
},
isHomeTeamProject(): boolean {
return this.homeProject?.type === ProjectTypes.Team;
},
credentialRoleTranslations(): Record<string, string> {
});
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': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
'credential:user': i18n.baseText('credentialEdit.credentialSharing.role.user'),
};
},
credentialRoles(): RoleMap['credential'] {
return this.rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
});
const credentialRoles = computed<RoleMap['credential']>(() => {
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
role,
name: this.credentialRoleTranslations[role],
name: credentialRoleTranslations.value[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 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,56 +1,60 @@
<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();
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
setFocus: [];
onBlur: [];
credentialSelected: [update: INodeUpdatePropertiesInformation];
}>();
const credentialsStore = useCredentialsStore();
const innerSelectRef = ref<HTMLSelectElement>();
const allCredentialTypes = computed(() => credentialsStore.allCredentialTypes);
const scopes = computed(() => {
if (!props.activeCredentialType) return [];
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
*/
isSupported(name: string): boolean {
const supported = this.getSupportedSets(this.parameter.credentialTypes);
function isSupported(name: string): boolean {
const supported = getSupportedSets(props.parameter.credentialTypes ?? []);
const checkedCredType = this.credentialsStore.getCredentialTypeByName(name);
const checkedCredType = credentialsStore.getCredentialTypeByName(name);
if (!checkedCredType) return false;
for (const property of supported.has) {
@ -73,14 +77,15 @@ export default defineComponent({
// recurse upward until base credential type
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
return checkedCredType.extends.reduce(
(acc: boolean, parentType: string) => acc || this.isSupported(parentType),
(acc: boolean, parentType: string) => acc || isSupported(parentType),
false,
);
}
return false;
},
getSupportedSets(credentialTypes: string[]) {
}
function getSupportedSets(credentialTypes: string[]) {
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
(acc, cur) => {
const _extends = cur.split('extends:');
@ -101,16 +106,16 @@ export default defineComponent({
},
{ 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() {
const telemetry = useTelemetry();
const modalBus = ref(createEventBus());
const selected = ref('');
const loading = ref(true);
const selectRef = ref<HTMLSelectElement>();
const credentialsStore = useCredentialsStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
onMounted(async () => {
try {
await this.credentialsStore.fetchCredentialTypes(false);
await credentialsStore.fetchCredentialTypes(false);
} catch (e) {}
this.loading = false;
loading.value = false;
setTimeout(() => {
const elementRef = this.$refs.select as HTMLSelectElement | undefined;
if (elementRef) {
elementRef.focus();
if (selectRef.value) {
selectRef.value.focus();
}
}, 0);
},
computed: {
...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore),
},
methods: {
onSelect(type: string) {
this.selected = type;
},
openCredentialType() {
this.modalBus.emit('close');
this.uiStore.openNewCredential(this.selected);
});
function onSelect(type: string) {
selected.value = type;
}
function openCredentialType() {
modalBus.value.emit('close');
uiStore.openNewCredential(selected.value);
const telemetryPayload = {
credential_type: this.selected,
credential_type: selected.value,
source: 'primary_menu',
new_credential: true,
workflow_id: this.workflowsStore.workflowId,
workflow_id: workflowsStore.workflowId,
};
this.$telemetry.track('User opened Credential modal', telemetryPayload);
void this.externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
},
},
});
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"

View file

@ -1,176 +1,158 @@
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IUser,
} from '@/Interface';
<script setup lang="ts">
import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
import type {
INodeCredentialDescription,
INodeCredentialsDetails,
INodeParameters,
INodeProperties,
INodeTypeDescription,
NodeParameterValueType,
} from 'n8n-workflow';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import TitledList from '@/components/TitledList.vue';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { ndvEventBus } from '@/event-bus';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store';
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { assert } from '@/utils/assert';
import {
getAllNodeCredentialForAuthType,
getAuthTypeForNodeCredential,
getMainAuthField,
getNodeCredentialForSelectedAuthType,
getAllNodeCredentialForAuthType,
updateNodeAuthType,
isRequiredCredential,
updateNodeAuthType,
} from '@/utils/nodeTypesUtils';
import { assert } from '@/utils/assert';
import { ndvEventBus } from '@/event-bus';
import {
N8nInput,
N8nInputLabel,
N8nOption,
N8nSelect,
N8nText,
N8nTooltip,
} from 'n8n-design-system';
interface CredentialDropdownOption extends ICredentialsResponse {
typeDisplayName: string;
}
export default defineComponent({
name: 'NodeCredentials',
components: {
TitledList,
},
props: {
readonly: {
type: Boolean,
default: false,
},
node: {
type: Object as PropType<INodeUi>,
required: true,
},
overrideCredType: {
type: String,
default: '',
},
showAll: {
type: Boolean,
default: false,
},
hideIssues: {
type: Boolean,
default: false,
},
},
emits: { credentialSelected: null, valueChanged: null, blur: null },
setup() {
type Props = {
node: INodeUi;
overrideCredType?: NodeParameterValueType;
readonly?: boolean;
showAll?: boolean;
hideIssues?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
readonly: false,
overrideCredType: '',
showAll: false,
hideIssues: false,
});
const emit = defineEmits<{
credentialSelected: [credential: INodeUpdatePropertiesInformation];
valueChanged: [value: { name: string; value: string }];
blur: [source: string];
}>();
const telemetry = useTelemetry();
const i18n = useI18n();
const NEW_CREDENTIALS_TEXT = `- ${i18n.baseText('nodeCredentials.createNew')} -`;
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const nodeHelpers = useNodeHelpers();
return {
...useToast(),
nodeHelpers,
};
},
data() {
return {
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`,
subscribedToCredentialType: '',
listeningForAuthChange: false,
};
},
computed: {
...mapStores(
useCredentialsStore,
useNodeTypesStore,
useNDVStore,
useUIStore,
useUsersStore,
useWorkflowsStore,
),
currentUser(): IUser {
return this.usersStore.currentUser ?? ({} as IUser);
},
credentialTypesNode(): string[] {
return this.credentialTypesNodeDescription.map(
const toast = useToast();
const subscribedToCredentialType = ref('');
const listeningForAuthChange = ref(false);
const credentialTypesNode = computed(() =>
credentialTypesNodeDescription.value.map(
(credentialTypeDescription) => credentialTypeDescription.name,
),
);
},
credentialTypesNodeDescriptionDisplayed(): INodeCredentialDescription[] {
return this.credentialTypesNodeDescription.filter((credentialTypeDescription) => {
return this.displayCredentials(credentialTypeDescription);
});
},
credentialTypesNodeDescription(): INodeCredentialDescription[] {
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType);
const credentialTypesNodeDescriptionDisplayed = computed(() =>
credentialTypesNodeDescription.value.filter((credentialTypeDescription) =>
displayCredentials(credentialTypeDescription),
),
);
const credentialTypesNodeDescription = computed(() => {
if (typeof props.overrideCredType !== 'string') return [];
const credType = credentialsStore.getCredentialTypeByName(props.overrideCredType);
if (credType) return [credType];
const activeNodeType = this.nodeType;
const activeNodeType = nodeType.value;
if (activeNodeType?.credentials) {
return activeNodeType.credentials;
}
return [];
},
credentialTypeNames() {
});
const credentialTypeNames = computed(() => {
const returnData: Record<string, string> = {};
for (const credentialTypeName of this.credentialTypesNode) {
const credentialType = this.credentialsStore.getCredentialTypeByName(credentialTypeName);
for (const credentialTypeName of credentialTypesNode.value) {
const credentialType = credentialsStore.getCredentialTypeByName(credentialTypeName);
returnData[credentialTypeName] = credentialType
? credentialType.displayName
: credentialTypeName;
}
return returnData;
},
selected(): Record<string, INodeCredentialsDetails> {
return this.node.credentials ?? {};
},
nodeType(): INodeTypeDescription | null {
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
},
mainNodeAuthField(): INodeProperties | null {
return getMainAuthField(this.nodeType);
},
},
watch: {
'node.parameters': {
immediate: true,
deep: true,
handler(newValue: INodeParameters, oldValue: INodeParameters) {
});
const selected = computed<Record<string, INodeCredentialsDetails>>(
() => props.node.credentials ?? {},
);
const nodeType = computed(() =>
nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion),
);
const mainNodeAuthField = computed(() => getMainAuthField(nodeType.value));
watch(
() => props.node.parameters,
(newValue, oldValue) => {
// When active node parameters change, check if authentication type has been changed
// and set `subscribedToCredentialType` to corresponding credential type
const isActive = this.node.name === this.ndvStore.activeNode?.name;
const nodeType = this.nodeType;
const isActive = props.node.name === ndvStore.activeNode?.name;
// Only do this for active node and if it's listening for auth change
if (isActive && nodeType && this.listeningForAuthChange) {
if (this.mainNodeAuthField && oldValue && newValue) {
const newAuth = newValue[this.mainNodeAuthField.name];
if (isActive && nodeType.value && listeningForAuthChange.value) {
if (mainNodeAuthField.value && oldValue && newValue) {
const newAuth = newValue[mainNodeAuthField.value.name];
if (newAuth) {
const authType =
typeof newAuth === 'object' ? JSON.stringify(newAuth) : newAuth.toString();
const credentialType = getNodeCredentialForSelectedAuthType(nodeType, authType);
const credentialType = getNodeCredentialForSelectedAuthType(nodeType.value, authType);
if (credentialType) {
this.subscribedToCredentialType = credentialType.name;
subscribedToCredentialType.value = credentialType.name;
}
}
}
}
},
},
},
mounted() {
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal
this.credentialsStore.$onAction(({ name, after, args }) => {
{ immediate: true, deep: true },
);
onMounted(() => {
credentialsStore.$onAction(({ name, after, args }) => {
const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];
const credentialType = this.subscribedToCredentialType;
const credentialType = subscribedToCredentialType.value;
if (!credentialType) {
return;
}
@ -179,42 +161,40 @@ export default defineComponent({
if (!listeningForActions.includes(name)) {
return;
}
const current = this.selected[credentialType];
const current = selected.value[credentialType];
let credentialsOfType: ICredentialsResponse[] = [];
if (this.showAll) {
if (this.node) {
credentialsOfType = [
...(this.credentialsStore.allUsableCredentialsForNode(this.node) || []),
];
if (props.showAll) {
if (props.node) {
credentialsOfType = [...(credentialsStore.allUsableCredentialsForNode(props.node) || [])];
}
} else {
credentialsOfType = [
...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []),
...(credentialsStore.allUsableCredentialsByType[credentialType] || []),
];
}
switch (name) {
// new credential was added
case 'createNewCredential':
if (result) {
this.onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
}
break;
case 'updateCredential':
const updatedCredential = result as ICredentialsResponse;
// credential name was changed, update it
if (updatedCredential.name !== current.name) {
this.onCredentialSelected(credentialType, current.id);
onCredentialSelected(credentialType, current.id);
}
break;
case 'deleteCredential':
// all credentials were deleted
if (credentialsOfType.length === 0) {
this.clearSelectedCredential(credentialType);
clearSelectedCredential(credentialType);
} else {
const id = args[0].id;
// credential was deleted, select last one added to replace with
if (current.id === id) {
this.onCredentialSelected(
onCredentialSelected(
credentialType,
credentialsOfType[credentialsOfType.length - 1].id,
);
@ -225,93 +205,83 @@ export default defineComponent({
});
});
ndvEventBus.on('credential.createNew', this.onCreateAndAssignNewCredential);
},
beforeUnmount() {
ndvEventBus.off('credential.createNew', this.onCreateAndAssignNewCredential);
},
methods: {
getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
const credentialIsRequired = this.showMixedCredentials(credentialType);
ndvEventBus.on('credential.createNew', onCreateAndAssignNewCredential);
});
onBeforeUnmount(() => {
ndvEventBus.off('credential.createNew', onCreateAndAssignNewCredential);
});
function getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
const credentialIsRequired = showMixedCredentials(credentialType);
if (credentialIsRequired) {
if (this.mainNodeAuthField) {
if (mainNodeAuthField.value) {
const credentials = getAllNodeCredentialForAuthType(
this.nodeType,
this.mainNodeAuthField.name,
nodeType.value,
mainNodeAuthField.value.name,
);
return credentials.map((cred) => cred.name);
}
}
return [credentialType.name];
},
getCredentialOptions(types: string[]): CredentialDropdownOption[] {
}
function getCredentialOptions(types: string[]): CredentialDropdownOption[] {
let options: CredentialDropdownOption[] = [];
types.forEach((type) => {
options = options.concat(
this.credentialsStore.allUsableCredentialsByType[type].map(
credentialsStore.allUsableCredentialsByType[type].map(
(option: ICredentialsResponse) =>
({
...option,
typeDisplayName: this.credentialsStore.getCredentialTypeByName(type)?.displayName,
typeDisplayName: credentialsStore.getCredentialTypeByName(type)?.displayName,
}) as CredentialDropdownOption,
),
);
});
return options;
},
getSelectedId(type: string) {
if (this.isCredentialExisting(type)) {
return this.selected[type].id;
}
function getSelectedId(type: string) {
if (isCredentialExisting(type)) {
return selected.value[type].id;
}
return undefined;
},
getSelectedName(type: string) {
return this.selected?.[type]?.name;
},
getSelectPlaceholder(type: string, issues: string[]) {
return issues.length && this.getSelectedName(type)
? this.$locale.baseText('nodeCredentials.selectedCredentialUnavailable', {
interpolate: { name: this.getSelectedName(type) },
}
function getSelectedName(type: string) {
return selected.value?.[type]?.name;
}
function getSelectPlaceholder(type: string, issues: string[]) {
return issues.length && getSelectedName(type)
? i18n.baseText('nodeCredentials.selectedCredentialUnavailable', {
interpolate: { name: getSelectedName(type) },
})
: this.$locale.baseText('nodeCredentials.selectCredential');
},
credentialInputWrapperStyle(credentialType: string) {
let deductWidth = 0;
const styles = {
width: '100%',
};
if (this.getIssues(credentialType).length) {
deductWidth += 20;
: i18n.baseText('nodeCredentials.selectCredential');
}
if (deductWidth !== 0) {
styles.width = `calc(100% - ${deductWidth}px)`;
}
return styles;
},
clearSelectedCredential(credentialType: string) {
const node: INodeUi = this.node;
function clearSelectedCredential(credentialType: string) {
const node = props.node;
const credentials = {
...(node.credentials || {}),
...(node.credentials ?? {}),
};
delete credentials[credentialType];
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
name: props.node.name,
properties: {
credentials,
position: this.node.position,
position: props.node.position,
},
};
this.$emit('credentialSelected', updateInformation);
},
emit('credentialSelected', updateInformation);
}
createNewCredential(
function createNewCredential(
credentialType: string,
listenForAuthChange: boolean = false,
showAuthOptions = false,
@ -319,76 +289,76 @@ export default defineComponent({
if (listenForAuthChange) {
// If new credential dialog is open, start listening for auth type change which should happen in the modal
// this will be handled in this component's watcher which will set subscribed credential accordingly
this.listeningForAuthChange = true;
this.subscribedToCredentialType = credentialType;
listeningForAuthChange.value = true;
subscribedToCredentialType.value = credentialType;
}
this.uiStore.openNewCredential(credentialType, showAuthOptions);
this.$telemetry.track('User opened Credential modal', {
uiStore.openNewCredential(credentialType, showAuthOptions);
telemetry.track('User opened Credential modal', {
credential_type: credentialType,
source: 'node',
new_credential: true,
workflow_id: this.workflowsStore.workflowId,
workflow_id: workflowsStore.workflowId,
});
},
}
onCreateAndAssignNewCredential({
function onCreateAndAssignNewCredential({
type,
showAuthOptions,
}: {
type: string;
showAuthOptions: boolean;
}) {
this.createNewCredential(type, true, showAuthOptions);
},
createNewCredential(type, true, showAuthOptions);
}
onCredentialSelected(
function onCredentialSelected(
credentialType: string,
credentialId: string | null | undefined,
showAuthOptions = false,
) {
const newCredentialOptionSelected = credentialId === this.NEW_CREDENTIALS_TEXT;
const newCredentialOptionSelected = credentialId === NEW_CREDENTIALS_TEXT;
if (!credentialId || newCredentialOptionSelected) {
this.createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
return;
}
this.$telemetry.track('User selected credential from node modal', {
telemetry.track('User selected credential from node modal', {
credential_type: credentialType,
node_type: this.node.type,
...(this.nodeHelpers.hasProxyAuth(this.node) ? { is_service_specific: true } : {}),
workflow_id: this.workflowsStore.workflowId,
node_type: props.node.type,
...(nodeHelpers.hasProxyAuth(props.node) ? { is_service_specific: true } : {}),
workflow_id: workflowsStore.workflowId,
credential_id: credentialId,
});
const selectedCredentials = this.credentialsStore.getCredentialById(credentialId);
const selectedCredentialsType = this.showAll ? selectedCredentials.type : credentialType;
const oldCredentials = this.node.credentials?.[selectedCredentialsType] ?? null;
const selectedCredentials = credentialsStore.getCredentialById(credentialId);
const selectedCredentialsType = props.showAll ? selectedCredentials.type : credentialType;
const oldCredentials = props.node.credentials?.[selectedCredentialsType] ?? null;
const selected = { id: selectedCredentials.id, name: selectedCredentials.name };
const newSelectedCredentials: INodeCredentialsDetails = {
id: selectedCredentials.id,
name: selectedCredentials.name,
};
// if credentials has been string or neither id matched nor name matched uniquely
if (
oldCredentials?.id === null ||
(oldCredentials?.id &&
!this.credentialsStore.getCredentialByIdAndType(
oldCredentials.id,
selectedCredentialsType,
))
!credentialsStore.getCredentialByIdAndType(oldCredentials.id, selectedCredentialsType))
) {
// update all nodes in the workflow with the same old/invalid credentials
this.workflowsStore.replaceInvalidWorkflowCredentials({
credentials: selected,
workflowsStore.replaceInvalidWorkflowCredentials({
credentials: newSelectedCredentials,
invalid: oldCredentials,
type: selectedCredentialsType,
});
this.nodeHelpers.updateNodesCredentialsIssues();
this.showMessage({
title: this.$locale.baseText('nodeCredentials.showMessage.title'),
message: this.$locale.baseText('nodeCredentials.showMessage.message', {
nodeHelpers.updateNodesCredentialsIssues();
toast.showMessage({
title: i18n.baseText('nodeCredentials.showMessage.title'),
message: i18n.baseText('nodeCredentials.showMessage.message', {
interpolate: {
oldCredentialName: oldCredentials.name,
newCredentialName: selected.name,
newCredentialName: newSelectedCredentials.name,
},
}),
type: 'success',
@ -396,54 +366,54 @@ export default defineComponent({
}
// If credential is selected from mixed credential dropdown, update node's auth filed based on selected credential
if (this.showAll && this.mainNodeAuthField) {
const nodeCredentialDescription = this.nodeType?.credentials?.find(
if (props.showAll && mainNodeAuthField.value) {
const nodeCredentialDescription = nodeType.value?.credentials?.find(
(cred) => cred.name === selectedCredentialsType,
);
const authOption = getAuthTypeForNodeCredential(this.nodeType, nodeCredentialDescription);
const authOption = getAuthTypeForNodeCredential(nodeType.value, nodeCredentialDescription);
if (authOption) {
updateNodeAuthType(this.node, authOption.value);
updateNodeAuthType(props.node, authOption.value);
const parameterData = {
name: `parameters.${this.mainNodeAuthField.name}`,
name: `parameters.${mainNodeAuthField.value.name}`,
value: authOption.value,
};
this.$emit('valueChanged', parameterData);
emit('valueChanged', parameterData);
}
}
const node: INodeUi = this.node;
const node = props.node;
const credentials = {
...(node.credentials ?? {}),
[selectedCredentialsType]: selected,
[selectedCredentialsType]: newSelectedCredentials,
};
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
name: props.node.name,
properties: {
credentials,
position: this.node.position,
position: props.node.position,
},
};
this.$emit('credentialSelected', updateInformation);
},
emit('credentialSelected', updateInformation);
}
displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean {
function displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean {
if (credentialTypeDescription.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.nodeHelpers.displayParameter(
this.node.parameters,
return nodeHelpers.displayParameter(
props.node.parameters,
credentialTypeDescription,
'',
this.node,
props.node,
);
},
}
getIssues(credentialTypeName: string): string[] {
const node = this.node;
function getIssues(credentialTypeName: string): string[] {
const node = props.node;
if (node.issues?.credentials === undefined) {
return [];
@ -453,58 +423,57 @@ export default defineComponent({
return [];
}
return node.issues.credentials[credentialTypeName];
},
}
isCredentialExisting(credentialType: string): boolean {
if (!this.node.credentials?.[credentialType]?.id) {
function isCredentialExisting(credentialType: string): boolean {
if (!props.node.credentials?.[credentialType]?.id) {
return false;
}
const { id } = this.node.credentials[credentialType];
const options = this.getCredentialOptions([credentialType]);
const { id } = props.node.credentials[credentialType];
const options = getCredentialOptions([credentialType]);
return !!options.find((option: ICredentialsResponse) => option.id === id);
},
}
editCredential(credentialType: string): void {
const credential = this.node.credentials?.[credentialType];
function editCredential(credentialType: string): void {
const credential = props.node.credentials?.[credentialType];
assert(credential?.id);
this.uiStore.openExistingCredential(credential.id);
uiStore.openExistingCredential(credential.id);
this.$telemetry.track('User opened Credential modal', {
telemetry.track('User opened Credential modal', {
credential_type: credentialType,
source: 'node',
new_credential: false,
workflow_id: this.workflowsStore.workflowId,
workflow_id: workflowsStore.workflowId,
});
this.subscribedToCredentialType = credentialType;
},
showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
const nodeType = this.nodeType;
const isRequired = isRequiredCredential(nodeType, credentialType);
subscribedToCredentialType.value = credentialType;
}
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired;
},
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
function showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
const isRequired = isRequiredCredential(nodeType.value, credentialType);
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(props.node.type ?? '') && isRequired;
}
function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
if (credentialType.displayName) return credentialType.displayName;
const credentialTypeName = this.credentialTypeNames[credentialType.name];
const isCredentialOnlyNode = this.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
const credentialTypeName = credentialTypeNames.value[credentialType.name];
const isCredentialOnlyNode = props.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
if (isCredentialOnlyNode) {
return this.$locale.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: this.nodeType?.displayName ?? credentialTypeName },
return i18n.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: nodeType.value?.displayName ?? credentialTypeName },
});
}
if (!this.showMixedCredentials(credentialType)) {
return this.$locale.baseText('nodeCredentials.credentialFor', {
if (!showMixedCredentials(credentialType)) {
return i18n.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: credentialTypeName },
});
}
return this.$locale.baseText('nodeCredentials.credentialsLabel');
},
},
});
return i18n.baseText('nodeCredentials.credentialsLabel');
}
</script>
<template>
@ -516,7 +485,7 @@ export default defineComponent({
v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed"
:key="credentialTypeDescription.name"
>
<n8n-input-label
<N8nInputLabel
:label="getCredentialsFieldLabel(credentialTypeDescription)"
:bold="false"
size="small"
@ -524,7 +493,7 @@ export default defineComponent({
data-test-id="credentials-label"
>
<div v-if="readonly">
<n8n-input
<N8nInput
:model-value="getSelectedName(credentialTypeDescription.name)"
disabled
size="small"
@ -540,7 +509,7 @@ export default defineComponent({
"
data-test-id="node-credentials-select"
>
<n8n-select
<N8nSelect
:model-value="getSelectedId(credentialTypeDescription.name)"
:placeholder="
getSelectPlaceholder(
@ -557,9 +526,9 @@ export default defineComponent({
showMixedCredentials(credentialTypeDescription),
)
"
@blur="$emit('blur', 'credentials')"
@blur="emit('blur', 'credentials')"
>
<n8n-option
<N8nOption
v-for="item in getCredentialOptions(
getAllRelatedCredentialTypes(credentialTypeDescription),
)"
@ -569,24 +538,24 @@ export default defineComponent({
:value="item.id"
>
<div :class="[$style.credentialOption, 'mt-2xs', 'mb-2xs']">
<n8n-text bold>{{ item.name }}</n8n-text>
<n8n-text size="small">{{ item.typeDisplayName }}</n8n-text>
<N8nText bold>{{ item.name }}</N8nText>
<N8nText size="small">{{ item.typeDisplayName }}</N8nText>
</div>
</n8n-option>
<n8n-option
</N8nOption>
<N8nOption
:key="NEW_CREDENTIALS_TEXT"
data-test-id="node-credentials-select-item-new"
:value="NEW_CREDENTIALS_TEXT"
:label="NEW_CREDENTIALS_TEXT"
>
</n8n-option>
</n8n-select>
</N8nOption>
</N8nSelect>
<div
v-if="getIssues(credentialTypeDescription.name).length && !hideIssues"
:class="$style.warning"
>
<n8n-tooltip placement="top">
<N8nTooltip placement="top">
<template #content>
<TitledList
:title="`${$locale.baseText('nodeCredentials.issues')}:`"
@ -594,7 +563,7 @@ export default defineComponent({
/>
</template>
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
</N8nTooltip>
</div>
<div
@ -613,7 +582,7 @@ export default defineComponent({
/>
</div>
</div>
</n8n-input-label>
</N8nInputLabel>
</div>
</div>
</template>