diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index c00e1b1835..9a4597f92a 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -132,6 +132,10 @@ export class License { return this.isFeatureEnabled(LICENSE_FEATURES.VERSION_CONTROL); } + isAPIDisabled() { + return this.isFeatureEnabled(LICENSE_FEATURES.API_DISABLED); + } + getCurrentEntitlements() { return this.manager?.getCurrentEntitlements() ?? []; } diff --git a/packages/cli/src/PublicApi/index.ts b/packages/cli/src/PublicApi/index.ts index fa27212db4..4ffa4d7fe5 100644 --- a/packages/cli/src/PublicApi/index.ts +++ b/packages/cli/src/PublicApi/index.ts @@ -16,6 +16,7 @@ import * as Db from '@/Db'; import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; +import { License } from '@/License'; async function createApiRouter( version: string, @@ -151,3 +152,12 @@ export const loadPublicApiVersions = async ( apiLatestVersion: Number(versions.pop()?.charAt(1)) ?? 1, }; }; + +function isApiEnabledByLicense(): boolean { + const license = Container.get(License); + return !license.isAPIDisabled(); +} + +export function isApiEnabled(): boolean { + return !config.get('publicApi.disabled') && isApiEnabledByLicense(); +} diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5406d2b2d..8739fe9ed1 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -97,7 +97,7 @@ import { import { executionsController } from '@/executions/executions.controller'; import { workflowStatsController } from '@/api/workflowStats.api'; -import { loadPublicApiVersions } from '@/PublicApi'; +import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi'; import { getInstanceBaseUrl, isEmailSetUp, @@ -277,7 +277,7 @@ export class Server extends AbstractServer { }, }, publicApi: { - enabled: !config.getEnv('publicApi.disabled'), + enabled: isApiEnabled(), latestVersion: 1, path: config.getEnv('publicApi.path'), swaggerUi: { @@ -538,7 +538,7 @@ export class Server extends AbstractServer { this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials, - config.getEnv('publicApi.disabled') ? publicApiEndpoint : '', + isApiEnabled() ? '' : publicApiEndpoint, ...excludeEndpoints.split(':'), ].filter((u) => !!u); @@ -564,7 +564,7 @@ export class Server extends AbstractServer { // Public API // ---------------------------------------- - if (!config.getEnv('publicApi.disabled')) { + if (isApiEnabled()) { const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(publicApiEndpoint); this.app.use(...apiRouters); this.frontendSettings.publicApi.latestVersion = apiLatestVersion; diff --git a/packages/cli/src/audit/risks/instance.risk.ts b/packages/cli/src/audit/risks/instance.risk.ts index 0bd0c18fb3..6d75acbd98 100644 --- a/packages/cli/src/audit/risks/instance.risk.ts +++ b/packages/cli/src/audit/risks/instance.risk.ts @@ -13,6 +13,7 @@ import { import { getN8nPackageJson, inDevelopment } from '@/constants'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { Risk, n8n } from '@/audit/types'; +import { isApiEnabled } from '@/PublicApi'; function getSecuritySettings() { if (config.getEnv('deployment.type') === 'cloud') return null; @@ -34,7 +35,7 @@ function getSecuritySettings() { communityPackagesEnabled: config.getEnv('nodes.communityPackages.enabled'), versionNotificationsEnabled: config.getEnv('versionNotifications.enabled'), templatesEnabled: config.getEnv('templates.enabled'), - publicApiEnabled: !config.getEnv('publicApi.disabled'), + publicApiEnabled: isApiEnabled(), userManagementEnabled, }; diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 818364bc5c..049f103cc9 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -78,6 +78,7 @@ export const enum LICENSE_FEATURES { ADVANCED_EXECUTION_FILTERS = 'feat:advancedExecutionFilters', VARIABLES = 'feat:variables', VERSION_CONTROL = 'feat:versionControl', + API_DISABLED = 'feat:apiDisabled', } export const enum LICENSE_QUOTAS { diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 127a1abd5f..ad899a10c6 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -38,7 +38,7 @@ export default defineComponent({ type: String, default: 'medium', validator: (value: string): boolean => - ['mini', 'small', 'medium', 'large', 'xlarge'].includes(value), + ['xmini', 'mini', 'small', 'medium', 'large', 'xlarge'].includes(value), }, loading: { type: Boolean, @@ -278,6 +278,17 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0); * Sizes */ +.xmini { + --button-padding-vertical: var(--spacing-4xs); + --button-padding-horizontal: var(--spacing-3xs); + --button-font-size: var(--font-size-3xs); + + &.square { + height: 22px; + width: 22px; + } +} + .mini { --button-padding-vertical: var(--spacing-4xs); --button-padding-horizontal: var(--spacing-2xs); diff --git a/packages/design-system/src/components/N8nMenu/Menu.vue b/packages/design-system/src/components/N8nMenu/Menu.vue index 66e9d61bb4..6dafe805fc 100644 --- a/packages/design-system/src/components/N8nMenu/Menu.vue +++ b/packages/design-system/src/components/N8nMenu/Menu.vue @@ -28,6 +28,7 @@
+ { + try { + await this.cloudPlanStore.getOwnerCurrentPLan(); + if (!this.cloudPlanStore.userIsTrialing) return; + await this.cloudPlanStore.getInstanceCurrentUsage(); + this.startPollingInstanceUsageData(); + } catch {} + }, + startPollingInstanceUsageData() { + const interval = setInterval(async () => { + try { + await this.cloudPlanStore.getInstanceCurrentUsage(); + if (this.cloudPlanStore.trialExpired || this.cloudPlanStore.allExecutionsUsed) { + clearTimeout(interval); + return; + } + } catch {} + }, CLOUD_TRIAL_CHECK_INTERVAL); + }, }, async mounted() { this.setTheme(); @@ -193,6 +216,7 @@ export default defineComponent({ this.authenticate(); this.redirectIfNecessary(); void this.checkForNewVersions(); + void this.checkForCloudPlanData(); this.loading = false; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index ee2365b8b8..3b9546127a 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1450,3 +1450,42 @@ export type VersionControlPreferences = { branchColor: string; publicKey?: string; }; + +export declare namespace Cloud { + export interface PlanData { + planId: number; + monthlyExecutionsLimit: number; + activeWorkflowsLimit: number; + credentialsLimit: number; + isActive: boolean; + displayName: string; + expirationDate: string; + metadata: PlanMetadata; + } + + export interface PlanMetadata { + version: 'v1'; + group: 'opt-out' | 'opt-in' | 'trial'; + slug: 'pro-1' | 'pro-2' | 'starter' | 'trial-1'; + trial?: Trial; + } + + interface Trial { + length: number; + gracePeriod: number; + } +} + +export interface CloudPlanState { + data: Cloud.PlanData | null; + usage: InstanceUsage | null; + loadingPlan: boolean; +} + +export interface InstanceUsage { + timeframe?: string; + executions: number; + activeWorkflows: number; +} + +export type CloudPlanAndUsageData = Cloud.PlanData & { usage: InstanceUsage }; diff --git a/packages/editor-ui/src/api/cloudPlans.ts b/packages/editor-ui/src/api/cloudPlans.ts new file mode 100644 index 0000000000..eb39bd42d9 --- /dev/null +++ b/packages/editor-ui/src/api/cloudPlans.ts @@ -0,0 +1,13 @@ +import type { Cloud, IRestApiContext, InstanceUsage } from '@/Interface'; +import { get } from '@/utils'; + +export async function getCurrentPlan( + context: IRestApiContext, + cloudUserId: string, +): Promise { + return get(context.baseUrl, `/user/${cloudUserId}/plan`); +} + +export async function getCurrentUsage(context: IRestApiContext): Promise { + return get(context.baseUrl, '/limits'); +} diff --git a/packages/editor-ui/src/components/ExecutionsUsage.vue b/packages/editor-ui/src/components/ExecutionsUsage.vue new file mode 100644 index 0000000000..791a82a669 --- /dev/null +++ b/packages/editor-ui/src/components/ExecutionsUsage.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index cc8ccb4f56..37ee67b81d 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -25,6 +25,12 @@ />
+ +