feat(cli): Enable community nodes based on npm availability (#3871)

*  Detect npm availability

* 📘 Expand interfaces

*  Adjust store

* 🎨 Replace button with warning
This commit is contained in:
Iván Ovejero 2022-08-11 16:39:55 +02:00 committed by GitHub
parent 620525ea85
commit 936264b3c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 56 additions and 10 deletions

View file

@ -516,6 +516,7 @@ export interface IN8nUISettings {
missingPackages?: boolean; missingPackages?: boolean;
executionMode: 'regular' | 'queue'; executionMode: 'regular' | 'queue';
communityNodesEnabled: boolean; communityNodesEnabled: boolean;
isNpmAvailable: boolean;
} }
export interface IPersonalizationSurveyAnswers { export interface IPersonalizationSurveyAnswers {

View file

@ -33,6 +33,7 @@
import express from 'express'; import express from 'express';
import { readFileSync, promises } from 'fs'; import { readFileSync, promises } from 'fs';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { exec as callbackExec } from 'child_process';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path'; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import { import {
@ -86,6 +87,7 @@ import jwks from 'jwks-rsa';
import timezones from 'google-timezones-json'; import timezones from 'google-timezones-json';
import parseUrl from 'parseurl'; import parseUrl from 'parseurl';
import promClient, { Registry } from 'prom-client'; import promClient, { Registry } from 'prom-client';
import { promisify } from 'util';
import * as Queue from './Queue'; import * as Queue from './Queue';
import { import {
ActiveExecutions, ActiveExecutions,
@ -167,6 +169,8 @@ import { loadPublicApiVersions } from './PublicApi';
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
const exec = promisify(callbackExec);
export const externalHooks: IExternalHooksClass = ExternalHooks(); export const externalHooks: IExternalHooksClass = ExternalHooks();
class App { class App {
@ -330,6 +334,7 @@ class App {
onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'), onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'),
executionMode: config.getEnv('executions.mode'), executionMode: config.getEnv('executions.mode'),
communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'), communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'),
isNpmAvailable: false,
}; };
} }
@ -374,6 +379,10 @@ class App {
promClient.collectDefaultMetrics({ register }); promClient.collectDefaultMetrics({ register });
} }
this.frontendSettings.isNpmAvailable = await exec('npm --version')
.then(() => true)
.catch(() => false);
this.versions = await GenericHelpers.getVersions(); this.versions = await GenericHelpers.getVersions();
this.frontendSettings.versionCli = this.versions.cli; this.frontendSettings.versionCli = this.versions.cli;

View file

@ -719,6 +719,7 @@ export interface IN8nUISettings {
}; };
executionMode: string; executionMode: string;
communityNodesEnabled: boolean; communityNodesEnabled: boolean;
isNpmAvailable: boolean;
publicApi: { publicApi: {
enabled: boolean; enabled: boolean;
latestVersion: number; latestVersion: number;
@ -881,6 +882,7 @@ export interface IRootState {
sidebarMenuItems: IMenuItem[]; sidebarMenuItems: IMenuItem[];
instanceId: string; instanceId: string;
nodeMetadata: {[nodeName: string]: INodeMetadata}; nodeMetadata: {[nodeName: string]: INodeMetadata};
isNpmAvailable: boolean;
} }
export interface ICommunityPackageMap { export interface ICommunityPackageMap {

View file

@ -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 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 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_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_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 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/`; export const CUSTOM_NODES_DOCS_URL = `https://docs.n8n.io/integrations/creating-nodes/code/create-n8n-nodes-module/`;

View file

@ -90,6 +90,9 @@ const module: Module<ISettingsState, IRootState> = {
isCommunityNodesFeatureEnabled: (state): boolean => { isCommunityNodesFeatureEnabled: (state): boolean => {
return state.settings.communityNodesEnabled; return state.settings.communityNodesEnabled;
}, },
isNpmAvailable: (state): boolean => {
return state.settings.isNpmAvailable;
},
isQueueModeEnabled: (state): boolean => { isQueueModeEnabled: (state): boolean => {
return state.settings.executionMode === 'queue'; return state.settings.executionMode === 'queue';
}, },
@ -138,6 +141,7 @@ const module: Module<ISettingsState, IRootState> = {
context.commit('setOauthCallbackUrls', settings.oauthCallbackUrls, {root: true}); context.commit('setOauthCallbackUrls', settings.oauthCallbackUrls, {root: true});
context.commit('setN8nMetadata', settings.n8nMetadata || {}, {root: true}); context.commit('setN8nMetadata', settings.n8nMetadata || {}, {root: true});
context.commit('setDefaultLocale', settings.defaultLocale, {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('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true});
context.commit('setCommunityNodesFeatureEnabled', settings.communityNodesEnabled === true); context.commit('setCommunityNodesFeatureEnabled', settings.communityNodesEnabled === true);
}, },

View file

@ -725,6 +725,7 @@
"settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community. <br /><a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">More info</a>", "settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community. <br /><a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">More info</a>",
"settings.communityNodes.empty.installPackageLabel": "Install a community node", "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. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">More info</a>", "settings.communityNodes.queueMode.warning": "You need to install community nodes manually because your instance is running in queue mode. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">More info</a>",
"settings.communityNodes.npmUnavailable.warning": "To use this feature, please <a href=\"{npmUrl}\" target=\"_blank\" title=\"How to install npm\">install npm</a> and restart n8n.",
"settings.communityNodes.packageNodes.label": "{count} node | {count} nodes", "settings.communityNodes.packageNodes.label": "{count} node | {count} nodes",
"settings.communityNodes.updateAvailable.tooltip": "A newer version is available", "settings.communityNodes.updateAvailable.tooltip": "A newer version is available",
"settings.communityNodes.viewDocsAction.label": "Documentation", "settings.communityNodes.viewDocsAction.label": "Documentation",

View file

@ -84,6 +84,7 @@ const state: IRootState = {
selectedNodes: [], selectedNodes: [],
sessionId: Math.random().toString(36).substring(2, 15), sessionId: Math.random().toString(36).substring(2, 15),
urlBaseWebhook: 'http://localhost:5678/', urlBaseWebhook: 'http://localhost:5678/',
isNpmAvailable: false,
workflow: { workflow: {
id: PLACEHOLDER_EMPTY_WORKFLOW_ID, id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
name: '', name: '',
@ -600,6 +601,9 @@ export const store = new Vuex.Store({
setDefaultLocale(state, locale: string) { setDefaultLocale(state, locale: string) {
Vue.set(state, 'defaultLocale', locale); Vue.set(state, 'defaultLocale', locale);
}, },
setIsNpmAvailable(state, isNpmAvailable: boolean) {
Vue.set(state, 'isNpmAvailable', isNpmAvailable);
},
setActiveNode(state, nodeName: string) { setActiveNode(state, nodeName: string) {
state.activeNode = nodeName; state.activeNode = nodeName;
}, },

View file

@ -36,7 +36,11 @@
<n8n-action-box <n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')" :heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription" :description="getEmptyStateDescription"
:buttonText="$locale.baseText('settings.communityNodes.empty.installPackageLabel')" :buttonText="
isNpmAvailable
? $locale.baseText('settings.communityNodes.empty.installPackageLabel')
: ''
"
:calloutText="actionBoxConfig.calloutText" :calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme" :calloutTheme="actionBoxConfig.calloutTheme"
@click="openInstallModal" @click="openInstallModal"
@ -63,7 +67,11 @@ import SettingsView from './SettingsView.vue';
import CommunityPackageCard from '../components/CommunityPackageCard.vue'; import CommunityPackageCard from '../components/CommunityPackageCard.vue';
import { showMessage } from '@/components/mixins/showMessage'; import { showMessage } from '@/components/mixins/showMessage';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '../constants'; import {
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
COMMUNITY_NODES_NPM_INSTALLATION_URL,
} from '../constants';
import { PublicInstalledPackage } from 'n8n-workflow'; import { PublicInstalledPackage } from 'n8n-workflow';
const PACKAGE_COUNT_THRESHOLD = 31; const PACKAGE_COUNT_THRESHOLD = 31;
@ -123,7 +131,7 @@ export default mixins(
} }
}, },
computed: { computed: {
...mapGetters('settings', ['isQueueModeEnabled']), ...mapGetters('settings', ['isNpmAvailable', 'isQueueModeEnabled']),
...mapGetters('communityNodes', ['getInstalledPackages']), ...mapGetters('communityNodes', ['getInstalledPackages']),
getEmptyStateDescription() { getEmptyStateDescription() {
const packageCount = this.$store.getters['communityNodes/availablePackageCount']; const packageCount = this.$store.getters['communityNodes/availablePackageCount'];
@ -141,13 +149,29 @@ export default mixins(
}); });
}, },
actionBoxConfig() { actionBoxConfig() {
return this.isQueueModeEnabled ? { if (!this.isNpmAvailable) {
calloutText: this.$locale.baseText('settings.communityNodes.queueMode.warning', { return {
interpolate: { docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL }, calloutText: this.$locale.baseText(
}), 'settings.communityNodes.npmUnavailable.warning',
calloutTheme: 'warning', { interpolate: { npmUrl: COMMUNITY_NODES_NPM_INSTALLATION_URL } },
hideButton: true, ),
} : { calloutTheme: 'warning',
hideButton: true,
};
}
if (this.isQueueModeEnabled) {
return {
calloutText: this.$locale.baseText(
'settings.communityNodes.queueMode.warning',
{ interpolate: { docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL } },
),
calloutTheme: 'warning',
hideButton: true,
};
}
return {
calloutText: '', calloutText: '',
calloutTheme: '', calloutTheme: '',
hideButton: false, hideButton: false,