feat: Enable cred setup for workflows created from templates (no-changelog) (#8240)

## Summary

Enable users to open credential setup for workflows that have been
created from templates if they skip it.

Next steps (will be their own PRs):
- Add telemetry events
- Add e2e test
- Hide the button when user sets up all the credentials
- Change the feature flag to a new one

## Related tickets and issues

https://linear.app/n8n/issue/ADO-1637/feature-support-template-credential-setup-for-http-request-nodes-that
This commit is contained in:
Tomi Turtiainen
2024-01-05 18:07:57 +02:00
committed by GitHub
parent df5d07bcb8
commit 3cf6704dbb
22 changed files with 858 additions and 560 deletions

View File

@@ -1,4 +1,3 @@
import sortBy from 'lodash-es/sortBy';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import type { Router } from 'vue-router';
@@ -7,22 +6,13 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow';
import type { ICredentialsResponse, INodeUi, IWorkflowTemplateNode } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import { VIEWS } from '@/constants';
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
import type {
TemplateCredentialKey,
TemplateNodeWithRequiredCredential,
} from '@/utils/templates/templateTransforms';
import {
getNodesRequiringCredentials,
keyFromCredentialTypeAndName,
normalizeTemplateNodeCredentials,
} from '@/utils/templates/templateTransforms';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry';
import { useCredentialSetupState } from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState';
export type NodeAndType = {
node: INodeUi;
@@ -35,131 +25,21 @@ export type RequiredCredentials = {
credentialType: string;
};
export type CredentialUsages = {
/**
* Key is a combination of the credential name and the credential type name,
* e.g. "twitter-twitterOAuth1Api"
*/
key: TemplateCredentialKey;
credentialName: string;
credentialType: string;
nodeTypeName: string;
usedBy: IWorkflowTemplateNode[];
};
export type AppCredentials = {
appName: string;
credentials: CredentialUsages[];
};
export type AppCredentialCount = {
appName: string;
count: number;
};
//#region Getter functions
export const groupNodeCredentialsByKey = (
nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[],
) => {
const credentialsByTypeName = new Map<TemplateCredentialKey, CredentialUsages>();
for (const { node, requiredCredentials } of nodeWithRequiredCredentials) {
const normalizedNodeCreds = node.credentials
? normalizeTemplateNodeCredentials(node.credentials)
: {};
for (const credentialDescription of requiredCredentials) {
const credentialType = credentialDescription.name;
const nodeCredentialName = normalizedNodeCreds[credentialDescription.name] ?? '';
const key = keyFromCredentialTypeAndName(credentialType, nodeCredentialName);
let credentialUsages = credentialsByTypeName.get(key);
if (!credentialUsages) {
credentialUsages = {
key,
nodeTypeName: node.type,
credentialName: nodeCredentialName,
credentialType,
usedBy: [],
};
credentialsByTypeName.set(key, credentialUsages);
}
credentialUsages.usedBy.push(node);
}
}
return credentialsByTypeName;
};
export const getAppCredentials = (
credentialUsages: CredentialUsages[],
getAppNameByNodeType: (nodeTypeName: string, version?: number) => string,
) => {
const credentialsByAppName = new Map<string, AppCredentials>();
for (const credentialUsage of credentialUsages) {
const nodeTypeName = credentialUsage.nodeTypeName;
const appName = getAppNameByNodeType(nodeTypeName) ?? nodeTypeName;
const appCredentials = credentialsByAppName.get(appName);
if (appCredentials) {
appCredentials.credentials.push(credentialUsage);
} else {
credentialsByAppName.set(appName, {
appName,
credentials: [credentialUsage],
});
}
}
return Array.from(credentialsByAppName.values());
};
export const getAppsRequiringCredentials = (
credentialUsagesByName: Map<string, CredentialUsages>,
getAppNameByNodeType: (nodeTypeName: string, version?: number) => string,
) => {
const credentialsByAppName = new Map<string, AppCredentialCount>();
for (const credentialUsage of credentialUsagesByName.values()) {
const node = credentialUsage.usedBy[0];
const appName = getAppNameByNodeType(node.type, node.typeVersion) ?? node.type;
const appCredentials = credentialsByAppName.get(appName);
if (appCredentials) {
appCredentials.count++;
} else {
credentialsByAppName.set(appName, {
appName,
count: 1,
});
}
}
return Array.from(credentialsByAppName.values());
};
//#endregion Getter functions
/**
* Store for managing the state of the SetupWorkflowFromTemplateView
*/
export const useSetupTemplateStore = defineStore('setupTemplate', () => {
//#region State
const templateId = ref<string>('');
const isLoading = ref(true);
const isSaving = ref(false);
/**
* Credentials user has selected from the UI. Map from credential
* name in the template to the credential ID.
*/
const selectedCredentialIdByKey = ref<
Record<CredentialUsages['key'], ICredentialsResponse['id']>
>({});
//#endregion State
const templatesStore = useTemplatesStore();
@@ -174,55 +54,21 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
return templateId.value ? templatesStore.getFullTemplateById(templateId.value) : null;
});
const nodesRequiringCredentialsSorted = computed(() => {
const nodesWithCredentials = template.value
? getNodesRequiringCredentials(nodeTypesStore, template.value)
: [];
// Order by the X coordinate of the node
return sortBy(nodesWithCredentials, ({ node }) => node.position[0]);
const templateNodes = computed(() => {
return template.value?.workflow.nodes ?? [];
});
const appNameByNodeType = (nodeTypeName: string, version?: number) => {
const nodeType = nodeTypesStore.getNodeType(nodeTypeName, version);
return nodeType ? getAppNameFromNodeName(nodeType.displayName) : nodeTypeName;
};
const credentialsByKey = computed(() => {
return groupNodeCredentialsByKey(nodesRequiringCredentialsSorted.value);
});
const credentialUsages = computed(() => {
return Array.from(credentialsByKey.value.values());
});
const appCredentials = computed(() => {
return getAppCredentials(credentialUsages.value, appNameByNodeType);
});
const credentialOverrides = computed(() => {
const overrides: Record<TemplateCredentialKey, INodeCredentialsDetails> = {};
for (const [key, credentialId] of Object.entries(selectedCredentialIdByKey.value)) {
const credential = credentialsStore.getCredentialById(credentialId);
if (!credential) {
continue;
}
// Object.entries fails to give the more accurate key type
overrides[key as TemplateCredentialKey] = {
id: credentialId,
name: credential.name,
};
}
return overrides;
});
const numFilledCredentials = computed(() => {
return Object.keys(selectedCredentialIdByKey.value).length;
});
const {
appCredentials,
credentialOverrides,
credentialUsages,
credentialsByKey,
nodesRequiringCredentialsSorted,
numFilledCredentials,
selectedCredentialIdByKey,
setSelectedCredentialId,
unsetSelectedCredential,
} = useCredentialSetupState(templateNodes);
//#endregion Getters
@@ -370,14 +216,6 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
}
};
const setSelectedCredentialId = (credentialKey: TemplateCredentialKey, credentialId: string) => {
selectedCredentialIdByKey.value[credentialKey] = credentialId;
};
const unsetSelectedCredential = (credentialKey: TemplateCredentialKey) => {
delete selectedCredentialIdByKey.value[credentialKey];
};
//#endregion Actions
return {