diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 64f7c0a473..984cc79839 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -516,6 +516,7 @@ export interface IN8nUISettings { missingPackages?: boolean; executionMode: 'regular' | 'queue'; communityNodesEnabled: boolean; + isNpmAvailable: boolean; } export interface IPersonalizationSurveyAnswers { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 379fbb3130..aad7b6207a 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -33,6 +33,7 @@ import express from 'express'; import { readFileSync, promises } from 'fs'; import { readFile } from 'fs/promises'; +import { exec as callbackExec } from 'child_process'; import _, { cloneDeep } from 'lodash'; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path'; import { @@ -86,6 +87,7 @@ import jwks from 'jwks-rsa'; import timezones from 'google-timezones-json'; import parseUrl from 'parseurl'; import promClient, { Registry } from 'prom-client'; +import { promisify } from 'util'; import * as Queue from './Queue'; import { ActiveExecutions, @@ -167,6 +169,8 @@ import { loadPublicApiVersions } from './PublicApi'; require('body-parser-xml')(bodyParser); +const exec = promisify(callbackExec); + export const externalHooks: IExternalHooksClass = ExternalHooks(); class App { @@ -330,6 +334,7 @@ class App { onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'), executionMode: config.getEnv('executions.mode'), communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'), + isNpmAvailable: false, }; } @@ -374,6 +379,10 @@ class App { promClient.collectDefaultMetrics({ register }); } + this.frontendSettings.isNpmAvailable = await exec('npm --version') + .then(() => true) + .catch(() => false); + this.versions = await GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 27f81463ec..c478c015e8 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -719,6 +719,7 @@ export interface IN8nUISettings { }; executionMode: string; communityNodesEnabled: boolean; + isNpmAvailable: boolean; publicApi: { enabled: boolean; latestVersion: number; @@ -881,6 +882,7 @@ export interface IRootState { sidebarMenuItems: IMenuItem[]; instanceId: string; nodeMetadata: {[nodeName: string]: INodeMetadata}; + isNpmAvailable: boolean; } export interface ICommunityPackageMap { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index ef7d63f622..423c6e18e6 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -63,6 +63,7 @@ export const NPM_PACKAGE_DOCS_BASE_URL = `https://www.npmjs.com/package/`; export const NPM_KEYWORD_SEARCH_URL = `https://www.npmjs.com/search?q=keywords%3An8n-community-node-package`; export const N8N_QUEUE_MODE_DOCS_URL = `https://docs.n8n.io/hosting/scaling/queue-mode/`; export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/installation/`; +export const COMMUNITY_NODES_NPM_INSTALLATION_URL = 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm'; export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/risks/`; export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/blocklist/`; export const CUSTOM_NODES_DOCS_URL = `https://docs.n8n.io/integrations/creating-nodes/code/create-n8n-nodes-module/`; diff --git a/packages/editor-ui/src/modules/settings.ts b/packages/editor-ui/src/modules/settings.ts index 1556dc9da3..a6df53caea 100644 --- a/packages/editor-ui/src/modules/settings.ts +++ b/packages/editor-ui/src/modules/settings.ts @@ -90,6 +90,9 @@ const module: Module = { isCommunityNodesFeatureEnabled: (state): boolean => { return state.settings.communityNodesEnabled; }, + isNpmAvailable: (state): boolean => { + return state.settings.isNpmAvailable; + }, isQueueModeEnabled: (state): boolean => { return state.settings.executionMode === 'queue'; }, @@ -138,6 +141,7 @@ const module: Module = { context.commit('setOauthCallbackUrls', settings.oauthCallbackUrls, {root: true}); context.commit('setN8nMetadata', settings.n8nMetadata || {}, {root: true}); context.commit('setDefaultLocale', settings.defaultLocale, {root: true}); + context.commit('setIsNpmAvailable', settings.isNpmAvailable, {root: true}); context.commit('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true}); context.commit('setCommunityNodesFeatureEnabled', settings.communityNodesEnabled === true); }, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 077580b48c..8718154626 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -725,6 +725,7 @@ "settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community.
More info", "settings.communityNodes.empty.installPackageLabel": "Install a community node", "settings.communityNodes.queueMode.warning": "You need to install community nodes manually because your instance is running in queue mode. More info", + "settings.communityNodes.npmUnavailable.warning": "To use this feature, please install npm and restart n8n.", "settings.communityNodes.packageNodes.label": "{count} node | {count} nodes", "settings.communityNodes.updateAvailable.tooltip": "A newer version is available", "settings.communityNodes.viewDocsAction.label": "Documentation", diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 62a1ed9fd1..62d256c2fd 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -84,6 +84,7 @@ const state: IRootState = { selectedNodes: [], sessionId: Math.random().toString(36).substring(2, 15), urlBaseWebhook: 'http://localhost:5678/', + isNpmAvailable: false, workflow: { id: PLACEHOLDER_EMPTY_WORKFLOW_ID, name: '', @@ -600,6 +601,9 @@ export const store = new Vuex.Store({ setDefaultLocale(state, locale: string) { Vue.set(state, 'defaultLocale', locale); }, + setIsNpmAvailable(state, isNpmAvailable: boolean) { + Vue.set(state, 'isNpmAvailable', isNpmAvailable); + }, setActiveNode(state, nodeName: string) { state.activeNode = nodeName; }, diff --git a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue index 0b5cc79c8a..dcfa2b78da 100644 --- a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -36,7 +36,11 @@