feat: Ado 1296 spike credential setup in templates (#7786)
- Add a 'Setup template credentials' view to setup the credentials of a template before it is created
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import sortBy from 'lodash-es/sortBy';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Router } from 'vue-router';
|
||||
import {
|
||||
useCredentialsStore,
|
||||
useNodeTypesStore,
|
||||
useRootStore,
|
||||
useTemplatesStore,
|
||||
useWorkflowsStore,
|
||||
} from '@/stores';
|
||||
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
|
||||
import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialsResponse,
|
||||
IExternalHooks,
|
||||
INodeUi,
|
||||
ITemplatesWorkflowFull,
|
||||
IWorkflowTemplateNode,
|
||||
} from '@/Interface';
|
||||
import type { Telemetry } from '@/plugins/telemetry';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
||||
import type { IWorkflowTemplateNodeWithCredentials } from '@/utils/templates/templateTransforms';
|
||||
import {
|
||||
hasNodeCredentials,
|
||||
normalizeTemplateNodeCredentials,
|
||||
} from '@/utils/templates/templateTransforms';
|
||||
|
||||
export type NodeAndType = {
|
||||
node: INodeUi;
|
||||
nodeType: INodeTypeDescription;
|
||||
};
|
||||
|
||||
export type RequiredCredentials = {
|
||||
node: INodeUi;
|
||||
credentialName: string;
|
||||
credentialType: string;
|
||||
};
|
||||
|
||||
export type CredentialUsages = {
|
||||
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 getNodesRequiringCredentials = (
|
||||
template: ITemplatesWorkflowFull,
|
||||
): IWorkflowTemplateNodeWithCredentials[] => {
|
||||
if (!template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return template.workflow.nodes.filter(hasNodeCredentials);
|
||||
};
|
||||
|
||||
export const groupNodeCredentialsByName = (nodes: IWorkflowTemplateNodeWithCredentials[]) => {
|
||||
const credentialsByName = new Map<string, CredentialUsages>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const normalizedCreds = normalizeTemplateNodeCredentials(node.credentials);
|
||||
for (const credentialType in normalizedCreds) {
|
||||
const credentialName = normalizedCreds[credentialType];
|
||||
|
||||
let credentialUsages = credentialsByName.get(credentialName);
|
||||
if (!credentialUsages) {
|
||||
credentialUsages = {
|
||||
nodeTypeName: node.type,
|
||||
credentialName,
|
||||
credentialType,
|
||||
usedBy: [],
|
||||
};
|
||||
credentialsByName.set(credentialName, credentialUsages);
|
||||
}
|
||||
|
||||
credentialUsages.usedBy.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return credentialsByName;
|
||||
};
|
||||
|
||||
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 selectedCredentialIdByName = ref<
|
||||
Record<CredentialUsages['credentialName'], ICredentialsResponse['id']>
|
||||
>({});
|
||||
|
||||
//#endregion State
|
||||
|
||||
const templatesStore = useTemplatesStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
//#region Getters
|
||||
|
||||
const template = computed(() => {
|
||||
return templateId.value ? templatesStore.getFullTemplateById(templateId.value) : null;
|
||||
});
|
||||
|
||||
const nodesRequiringCredentialsSorted = computed(() => {
|
||||
const credentials = template.value ? getNodesRequiringCredentials(template.value) : [];
|
||||
|
||||
// Order by the X coordinate of the node
|
||||
return sortBy(credentials, ({ position }) => position[0]);
|
||||
});
|
||||
|
||||
const appNameByNodeType = (nodeTypeName: string, version?: number) => {
|
||||
const nodeType = nodeTypesStore.getNodeType(nodeTypeName, version);
|
||||
|
||||
return nodeType ? getAppNameFromNodeName(nodeType.displayName) : nodeTypeName;
|
||||
};
|
||||
|
||||
const credentialsByName = computed(() => {
|
||||
return groupNodeCredentialsByName(nodesRequiringCredentialsSorted.value);
|
||||
});
|
||||
|
||||
const credentialUsages = computed(() => {
|
||||
return Array.from(credentialsByName.value.values());
|
||||
});
|
||||
|
||||
const appCredentials = computed(() => {
|
||||
return getAppCredentials(credentialUsages.value, appNameByNodeType);
|
||||
});
|
||||
|
||||
const credentialOverrides = computed(() => {
|
||||
const overrides: Record<string, INodeCredentialsDetails> = {};
|
||||
|
||||
for (const credentialNameInTemplate of Object.keys(selectedCredentialIdByName.value)) {
|
||||
const credentialId = selectedCredentialIdByName.value[credentialNameInTemplate];
|
||||
if (!credentialId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const credential = credentialsStore.getCredentialById(credentialId);
|
||||
if (!credential) {
|
||||
continue;
|
||||
}
|
||||
|
||||
overrides[credentialNameInTemplate] = {
|
||||
id: credentialId,
|
||||
name: credential.name,
|
||||
};
|
||||
}
|
||||
|
||||
return overrides;
|
||||
});
|
||||
|
||||
const numCredentialsLeft = computed(() => {
|
||||
return credentialUsages.value.length - Object.keys(selectedCredentialIdByName.value).length;
|
||||
});
|
||||
|
||||
//#endregion Getters
|
||||
|
||||
//#region Actions
|
||||
|
||||
const setTemplateId = (id: string) => {
|
||||
templateId.value = id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the template if it hasn't been loaded yet.
|
||||
*/
|
||||
const loadTemplateIfNeeded = async () => {
|
||||
if (!!template.value || !templateId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await templatesStore.fetchTemplateById(templateId.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the store for a specific template.
|
||||
*/
|
||||
const init = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
selectedCredentialIdByName.value = {};
|
||||
|
||||
await Promise.all([
|
||||
credentialsStore.fetchAllCredentials(),
|
||||
credentialsStore.fetchCredentialTypes(false),
|
||||
nodeTypesStore.loadNodeTypesIfNotLoaded(),
|
||||
loadTemplateIfNeeded(),
|
||||
]);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Skips the setup and goes directly to the workflow view.
|
||||
*/
|
||||
const skipSetup = async (opts: {
|
||||
$externalHooks: IExternalHooks;
|
||||
$telemetry: Telemetry;
|
||||
$router: Router;
|
||||
}) => {
|
||||
const { $externalHooks, $telemetry, $router } = opts;
|
||||
const telemetryPayload = {
|
||||
source: 'workflow',
|
||||
template_id: templateId.value,
|
||||
wf_template_repo_session_id: templatesStore.currentSessionId,
|
||||
};
|
||||
|
||||
await $externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload);
|
||||
$telemetry.track('User inserted workflow template', telemetryPayload, {
|
||||
withPostHog: true,
|
||||
});
|
||||
|
||||
// Replace the URL so back button doesn't come back to this setup view
|
||||
await $router.replace({
|
||||
name: VIEWS.TEMPLATE_IMPORT,
|
||||
params: { id: templateId.value },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a workflow from the template and navigates to the workflow view.
|
||||
*/
|
||||
const createWorkflow = async ($router: Router) => {
|
||||
if (!template.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isSaving.value = true;
|
||||
|
||||
const createdWorkflow = await createWorkflowFromTemplate(
|
||||
template.value,
|
||||
credentialOverrides.value,
|
||||
rootStore,
|
||||
workflowsStore,
|
||||
);
|
||||
|
||||
// Replace the URL so back button doesn't come back to this setup view
|
||||
await $router.replace({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: createdWorkflow.id },
|
||||
});
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectedCredentialId = (credentialName: string, credentialId: string) => {
|
||||
selectedCredentialIdByName.value[credentialName] = credentialId;
|
||||
};
|
||||
|
||||
const unsetSelectedCredential = (credentialName: string) => {
|
||||
delete selectedCredentialIdByName.value[credentialName];
|
||||
};
|
||||
|
||||
//#endregion Actions
|
||||
|
||||
return {
|
||||
credentialsByName,
|
||||
isLoading,
|
||||
isSaving,
|
||||
appCredentials,
|
||||
nodesRequiringCredentialsSorted,
|
||||
template,
|
||||
credentialUsages,
|
||||
selectedCredentialIdByName,
|
||||
numCredentialsLeft,
|
||||
createWorkflow,
|
||||
skipSetup,
|
||||
init,
|
||||
loadTemplateIfNeeded,
|
||||
setTemplateId,
|
||||
setSelectedCredentialId,
|
||||
unsetSelectedCredential,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user