## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. As part of NodeView refactor, this PR migrates all externalHooks calls to `useExternalHooks` composable. #### How to test the change: 1. Run using env `export N8N_DEPLOYMENT_TYPE=cloud` 2. Hooks should still run as expected ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers https://linear.app/n8n/issue/N8N-6349/externalhooks ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e).
383 lines
10 KiB
TypeScript
383 lines
10 KiB
TypeScript
import sortBy from 'lodash-es/sortBy';
|
|
import { defineStore } from 'pinia';
|
|
import { computed, ref } from 'vue';
|
|
import type { Router } from 'vue-router';
|
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
|
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,
|
|
ITemplatesWorkflowFull,
|
|
IWorkflowTemplateNode,
|
|
} from '@/Interface';
|
|
import { VIEWS } from '@/constants';
|
|
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
|
import type {
|
|
TemplateCredentialKey,
|
|
IWorkflowTemplateNodeWithCredentials,
|
|
} from '@/utils/templates/templateTransforms';
|
|
import {
|
|
hasNodeCredentials,
|
|
keyFromCredentialTypeAndName,
|
|
normalizeTemplateNodeCredentials,
|
|
} from '@/utils/templates/templateTransforms';
|
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
|
|
export type NodeAndType = {
|
|
node: INodeUi;
|
|
nodeType: INodeTypeDescription;
|
|
};
|
|
|
|
export type RequiredCredentials = {
|
|
node: INodeUi;
|
|
credentialName: string;
|
|
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 getNodesRequiringCredentials = (
|
|
template: ITemplatesWorkflowFull,
|
|
): IWorkflowTemplateNodeWithCredentials[] => {
|
|
if (!template) {
|
|
return [];
|
|
}
|
|
|
|
return template.workflow.nodes.filter(hasNodeCredentials);
|
|
};
|
|
|
|
export const groupNodeCredentialsByKey = (nodes: IWorkflowTemplateNodeWithCredentials[]) => {
|
|
const credentialsByTypeName = new Map<TemplateCredentialKey, CredentialUsages>();
|
|
|
|
for (const node of nodes) {
|
|
const normalizedCreds = normalizeTemplateNodeCredentials(node.credentials);
|
|
for (const credentialType in normalizedCreds) {
|
|
const credentialName = normalizedCreds[credentialType];
|
|
const key = keyFromCredentialTypeAndName(credentialType, credentialName);
|
|
|
|
let credentialUsages = credentialsByTypeName.get(key);
|
|
if (!credentialUsages) {
|
|
credentialUsages = {
|
|
key,
|
|
nodeTypeName: node.type,
|
|
credentialName,
|
|
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();
|
|
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 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;
|
|
});
|
|
|
|
//#endregion Getters
|
|
|
|
//#region Actions
|
|
|
|
const setTemplateId = (id: string) => {
|
|
templateId.value = id;
|
|
};
|
|
|
|
const ignoredAutoFillCredentialTypes = new Set([
|
|
'httpBasicAuth',
|
|
'httpCustomAuth',
|
|
'httpDigestAuth',
|
|
'httpHeaderAuth',
|
|
'oAuth1Api',
|
|
'oAuth2Api',
|
|
'httpQueryAuth',
|
|
]);
|
|
|
|
/**
|
|
* Selects initial credentials for the template. Credentials
|
|
* need to be loaded before this.
|
|
*/
|
|
const setInitialCredentialSelection = () => {
|
|
for (const credUsage of credentialUsages.value) {
|
|
if (ignoredAutoFillCredentialTypes.has(credUsage.credentialType)) {
|
|
continue;
|
|
}
|
|
|
|
const availableCreds = credentialsStore.getCredentialsByType(credUsage.credentialType);
|
|
|
|
if (availableCreds.length === 1) {
|
|
selectedCredentialIdByKey.value[credUsage.key] = availableCreds[0].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);
|
|
|
|
setInitialCredentialSelection();
|
|
};
|
|
|
|
/**
|
|
* Initializes the store for a specific template.
|
|
*/
|
|
const init = async () => {
|
|
isLoading.value = true;
|
|
try {
|
|
selectedCredentialIdByKey.value = {};
|
|
|
|
await Promise.all([
|
|
credentialsStore.fetchAllCredentials(),
|
|
credentialsStore.fetchCredentialTypes(false),
|
|
nodeTypesStore.loadNodeTypesIfNotLoaded(),
|
|
loadTemplateIfNeeded(),
|
|
]);
|
|
|
|
setInitialCredentialSelection();
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Skips the setup and goes directly to the workflow view.
|
|
*/
|
|
const skipSetup = async ({ router }: { router: Router }) => {
|
|
const externalHooks = useExternalHooks();
|
|
const telemetry = useTelemetry();
|
|
|
|
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 = (credentialKey: TemplateCredentialKey, credentialId: string) => {
|
|
selectedCredentialIdByKey.value[credentialKey] = credentialId;
|
|
};
|
|
|
|
const unsetSelectedCredential = (credentialKey: TemplateCredentialKey) => {
|
|
delete selectedCredentialIdByKey.value[credentialKey];
|
|
};
|
|
|
|
//#endregion Actions
|
|
|
|
return {
|
|
credentialsByKey,
|
|
isLoading,
|
|
isSaving,
|
|
appCredentials,
|
|
nodesRequiringCredentialsSorted,
|
|
template,
|
|
credentialUsages,
|
|
selectedCredentialIdByKey,
|
|
credentialOverrides,
|
|
createWorkflow,
|
|
skipSetup,
|
|
init,
|
|
loadTemplateIfNeeded,
|
|
setInitialCredentialSelection,
|
|
setTemplateId,
|
|
setSelectedCredentialId,
|
|
unsetSelectedCredential,
|
|
};
|
|
});
|