Files
n8n-enterprise-unlocked/packages/cli/src/services/frontend.service.ts
Michael Auerswald cbc690907f feat(editor): Adds a EE view to show worker details and job status (#7600)
This change expands on the command channel communication introduced
lately between the main instance(s) and the workers. The frontend gets a
new menu entry "Workers" which will, when opened, trigger a regular call
to getStatus from the workers. The workers then respond via their
response channel to the backend, which then pushes the status to the
frontend.
This introduces the use of ChartJS for metrics.
This feature is still in MVP state and thus disabled by default for the
moment.
2023-11-10 23:48:31 +01:00

347 lines
12 KiB
TypeScript

import { Container, Service } from 'typedi';
import uniq from 'lodash/uniq';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';
import type {
ICredentialType,
IN8nUISettings,
INodeTypeBaseDescription,
ITelemetrySettings,
} from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';
import { LICENSE_FEATURES } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { License } from '@/License';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import config from '@/config';
import { getCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { getLdapLoginLabel } from '@/Ldap/helpers';
import { getSamlLoginLabel } from '@/sso/saml/samlHelpers';
import { getVariablesLimit } from '@/environments/variables/enviromentHelpers';
import {
getWorkflowHistoryLicensePruneTime,
getWorkflowHistoryPruneTime,
} from '@/workflows/workflowHistory/workflowHistoryHelper.ee';
import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger';
@Service()
export class FrontendService {
settings: IN8nUISettings;
private communityPackagesService?: CommunityPackagesService;
constructor(
private readonly logger: Logger,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly credentialTypes: CredentialTypes,
private readonly credentialsOverwrites: CredentialsOverwrites,
private readonly license: License,
private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings,
) {
loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes());
void this.generateTypes();
this.initSettings();
if (config.getEnv('nodes.communityPackages.enabled')) {
// eslint-disable-next-line @typescript-eslint/naming-convention
void import('@/services/communityPackages.service').then(({ CommunityPackagesService }) => {
this.communityPackagesService = Container.get(CommunityPackagesService);
});
}
}
private initSettings() {
const instanceBaseUrl = getInstanceBaseUrl();
const restEndpoint = config.getEnv('endpoints.rest');
const telemetrySettings: ITelemetrySettings = {
enabled: config.getEnv('diagnostics.enabled'),
};
if (telemetrySettings.enabled) {
const conf = config.getEnv('diagnostics.config.frontend');
const [key, url] = conf.split(';');
if (!key || !url) {
this.logger.warn('Diagnostics frontend config is invalid');
telemetrySettings.enabled = false;
}
telemetrySettings.config = { key, url };
}
this.settings = {
endpointWebhook: config.getEnv('endpoints.webhook'),
endpointWebhookTest: config.getEnv('endpoints.webhookTest'),
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
executionTimeout: config.getEnv('executions.timeout'),
maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
timezone: config.getEnv('generic.timezone'),
urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(),
urlBaseEditor: instanceBaseUrl,
versionCli: '',
releaseChannel: config.getEnv('generic.releaseChannel'),
oauthCallbackUrls: {
oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
},
versionNotifications: {
enabled: config.getEnv('versionNotifications.enabled'),
endpoint: config.getEnv('versionNotifications.endpoint'),
infoUrl: config.getEnv('versionNotifications.infoUrl'),
},
instanceId: this.instanceSettings.instanceId,
telemetry: telemetrySettings,
posthog: {
enabled: config.getEnv('diagnostics.enabled'),
apiHost: config.getEnv('diagnostics.config.posthog.apiHost'),
apiKey: config.getEnv('diagnostics.config.posthog.apiKey'),
autocapture: false,
disableSessionRecording: config.getEnv(
'diagnostics.config.posthog.disableSessionRecording',
),
debug: config.getEnv('logs.level') === 'debug',
},
personalizationSurveyEnabled:
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
userManagement: {
quota: this.license.getUsersLimit(),
showSetupOnFirstLoad: !config.getEnv('userManagement.isInstanceOwnerSetUp'),
smtpSetup: this.mailer.isEmailSetUp,
authenticationMethod: getCurrentAuthenticationMethod(),
},
sso: {
saml: {
loginEnabled: false,
loginLabel: '',
},
ldap: {
loginEnabled: false,
loginLabel: '',
},
},
publicApi: {
enabled: !config.get('publicApi.disabled') && !this.license.isAPIDisabled(),
latestVersion: 1,
path: config.getEnv('publicApi.path'),
swaggerUi: {
enabled: !config.getEnv('publicApi.swaggerUi.disabled'),
},
},
workflowTagsDisabled: config.getEnv('workflowTagsDisabled'),
logLevel: config.getEnv('logs.level'),
hiringBannerEnabled: config.getEnv('hiringBanner.enabled'),
templates: {
enabled: config.getEnv('templates.enabled'),
host: config.getEnv('templates.host'),
},
onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'),
executionMode: config.getEnv('executions.mode'),
pushBackend: config.getEnv('push.backend'),
communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'),
deployment: {
type: config.getEnv('deployment.type'),
},
isNpmAvailable: false,
allowedModules: {
builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN?.split(',') ?? undefined,
external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL?.split(',') ?? undefined,
},
enterprise: {
sharing: false,
ldap: false,
saml: false,
logStreaming: false,
advancedExecutionFilters: false,
variables: false,
sourceControl: false,
auditLogs: false,
externalSecrets: false,
showNonProdBanner: false,
debugInEditor: false,
binaryDataS3: false,
workflowHistory: false,
workerView: false,
},
mfa: {
enabled: false,
},
hideUsagePage: config.getEnv('hideUsagePage'),
license: {
environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging',
},
variables: {
limit: 0,
},
expressions: {
evaluator: config.getEnv('expression.evaluator'),
},
banners: {
dismissed: [],
},
ai: {
enabled: config.getEnv('ai.enabled'),
},
workflowHistory: {
pruneTime: -1,
licensePruneTime: -1,
},
};
}
async generateTypes() {
this.overwriteCredentialsProperties();
const { staticCacheDir } = this.instanceSettings;
// pre-render all the node and credential types as static json files
await mkdir(path.join(staticCacheDir, 'types'), { recursive: true });
const { credentials, nodes } = this.loadNodesAndCredentials.types;
this.writeStaticJSON('nodes', nodes);
this.writeStaticJSON('credentials', credentials);
}
getSettings(): IN8nUISettings {
const restEndpoint = config.getEnv('endpoints.rest');
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
const instanceBaseUrl = getInstanceBaseUrl();
this.settings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
this.settings.urlBaseEditor = instanceBaseUrl;
this.settings.oauthCallbackUrls = {
oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
};
// refresh user management status
Object.assign(this.settings.userManagement, {
quota: this.license.getUsersLimit(),
authenticationMethod: getCurrentAuthenticationMethod(),
showSetupOnFirstLoad:
!config.getEnv('userManagement.isInstanceOwnerSetUp') &&
!config.getEnv('deployment.type').startsWith('desktop_'),
});
let dismissedBanners: string[] = [];
try {
dismissedBanners = config.getEnv('ui.banners.dismissed') ?? [];
} catch {
// not yet in DB
}
this.settings.banners.dismissed = dismissedBanners;
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed();
// refresh enterprise status
Object.assign(this.settings.enterprise, {
sharing: this.license.isSharingEnabled(),
logStreaming: this.license.isLogStreamingEnabled(),
ldap: this.license.isLdapEnabled(),
saml: this.license.isSamlEnabled(),
advancedExecutionFilters: this.license.isAdvancedExecutionFiltersEnabled(),
variables: this.license.isVariablesEnabled(),
sourceControl: this.license.isSourceControlLicensed(),
externalSecrets: this.license.isExternalSecretsEnabled(),
showNonProdBanner: this.license.isFeatureEnabled(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
debugInEditor: this.license.isDebugInEditorLicensed(),
binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
workflowHistory:
this.license.isWorkflowHistoryLicensed() && config.getEnv('workflowHistory.enabled'),
workerView: this.license.isWorkerViewLicensed(),
});
if (this.license.isLdapEnabled()) {
Object.assign(this.settings.sso.ldap, {
loginLabel: getLdapLoginLabel(),
loginEnabled: config.getEnv('sso.ldap.loginEnabled'),
});
}
if (this.license.isSamlEnabled()) {
Object.assign(this.settings.sso.saml, {
loginLabel: getSamlLoginLabel(),
loginEnabled: config.getEnv('sso.saml.loginEnabled'),
});
}
if (this.license.isVariablesEnabled()) {
this.settings.variables.limit = getVariablesLimit();
}
if (this.license.isWorkflowHistoryLicensed() && config.getEnv('workflowHistory.enabled')) {
Object.assign(this.settings.workflowHistory, {
pruneTime: getWorkflowHistoryPruneTime(),
licensePruneTime: getWorkflowHistoryLicensePruneTime(),
});
}
if (this.communityPackagesService) {
this.settings.missingPackages = this.communityPackagesService.hasMissingPackages;
}
this.settings.mfa.enabled = config.get('mfa.enabled');
this.settings.executionMode = config.getEnv('executions.mode');
return this.settings;
}
addToSettings(newSettings: Record<string, unknown>) {
this.settings = { ...this.settings, ...newSettings };
}
private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) {
const { staticCacheDir } = this.instanceSettings;
const filePath = path.join(staticCacheDir, `types/${name}.json`);
const stream = createWriteStream(filePath, 'utf-8');
stream.write('[\n');
data.forEach((entry, index) => {
stream.write(JSON.stringify(entry));
if (index !== data.length - 1) stream.write(',');
stream.write('\n');
});
stream.write(']\n');
stream.end();
}
private overwriteCredentialsProperties() {
const { credentials } = this.loadNodesAndCredentials.types;
const credentialsOverwrites = this.credentialsOverwrites.getAll();
for (const credential of credentials) {
const overwrittenProperties = [];
this.credentialTypes
.getParentTypes(credential.name)
.reverse()
.map((name) => credentialsOverwrites[name])
.forEach((overwrite) => {
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
});
if (credential.name in credentialsOverwrites) {
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
}
if (overwrittenProperties.length) {
credential.__overwrittenProperties = uniq(overwrittenProperties);
}
}
}
}