Skip to content

Commit

Permalink
feat(editor): Add cloud ExecutionsUsage and API blocking using licens…
Browse files Browse the repository at this point in the history
…es (#6159)

* Add ExecutionsUsage component

* set $sidebar-expanded-width back to 200px

* add days using interpolation

* Rename PlanData type to CloudPlanData

* Rename Metadata type to PlanMetadata

* Make prop block in the update button

* Use variable in line-height

* Remove progressBarSection class

* fix trial expiration calculation

* mock expirationDate and fix issue with days left

* Remove unnecesary property from class .container

* inject component data via props

* Check for plan data during app mounting and keep data in the store

* Remove mounted hook

* redirect when upgrade plan is clicked

* Remove computed properties

* Remove instance property as it's not needed anymore

* Flatten plan object

* remove console.log

* Add all cloud types within its own namespace

* keep redirection inside component

* get computed properties back

* Improve polling logic

* Move cloudData to its own store

* Remove commented interfaces

* remove cloudPlan from user store

* fix imports

* update logic for userIsTrialing method

* centralize userIsTrialing method

* redirect to production change plan page always

* Call staging or production cloud api depending on base URL

* remove setting store form ExecutionUsage.vue

* fix linting issue

* Add trial group to PlanMetadata group

* Move helpers into the store

* make staging url check more specific

* make cloud state nullable

* fix linting issue

* swap mockup date for endpoint

* Make getCurrentPlan async

* asas

* Improvements

* small improvements

* chore: resolve conflicts

* make sure there is data before calculating trial expiration

* Fix issue with component not loading on first page load

* type safety improvements

* apply component ui feedback

* fix linting issue

* chore: clean up unnecessary change from merge conflict

* feat: Block api feature using licenses, show notice page for trial cloud users (#6187)

* rename planSpec to plan

* Remove instance property as it's not needed anymore

* Flatten plan object

* remove console.log

* feat: disable api using license

* feat: add api page

* chore: resolve conflicts

* chore: resolve conflicts

* feat: update and refactor a bit

* fix: update endpoints

* fix: update endpoints

* fix: use host

* feat: update copy

* fix linting issues

---------

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>

* add pluralization to days left text

---------

Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
  • Loading branch information
3 people committed May 15, 2023
1 parent 51fb913 commit cd7c312
Show file tree
Hide file tree
Showing 20 changed files with 480 additions and 15 deletions.
4 changes: 4 additions & 0 deletions packages/cli/src/License.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() ?? [];
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/PublicApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
8 changes: 4 additions & 4 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/audit/risks/instance.risk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion packages/design-system/src/components/N8nButton/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/design-system/src/components/N8nMenu/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</el-menu>
</div>
<div :class="[$style.lowerContent, 'pb-2xs']">
<slot name="beforeLowerMenu"></slot>
<el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
<n8n-menu-item
v-for="item in lowerMenuItems"
Expand Down
26 changes: 25 additions & 1 deletion packages/editor-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import Modals from '@/components/Modals.vue';
import LoadingView from '@/views/LoadingView.vue';
import Telemetry from '@/components/Telemetry.vue';
import { HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } from '@/constants';
import { CLOUD_TRIAL_CHECK_INTERVAL, HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } from '@/constants';
import { userHelpers } from '@/mixins/userHelpers';
import { loadLanguage } from '@/plugins/i18n';
Expand All @@ -42,10 +42,12 @@ import { useUsersStore } from '@/stores/users.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCloudPlanStore } from './stores/cloudPlan.store';
import { useHistoryHelper } from '@/composables/useHistoryHelper';
import { newVersions } from '@/mixins/newVersions';
import { useRoute } from 'vue-router/composables';
import { useVersionControlStore } from '@/stores/versionControl.store';
import { useUsageStore } from '@/stores/usage.store';
import { useExternalHooks } from '@/composables';
import { defineComponent } from 'vue';
Expand Down Expand Up @@ -75,6 +77,8 @@ export default defineComponent({
useUIStore,
useUsersStore,
useVersionControlStore,
useCloudPlanStore,
useUsageStore,
),
defaultLocale(): string {
return this.rootStore.defaultLocale;
Expand Down Expand Up @@ -185,6 +189,25 @@ export default defineComponent({
window.document.body.classList.add(`theme-${theme}`);
}
},
async checkForCloudPlanData(): Promise<void> {
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();
Expand All @@ -193,6 +216,7 @@ export default defineComponent({
this.authenticate();
this.redirectIfNecessary();
void this.checkForNewVersions();
void this.checkForCloudPlanData();
this.loading = false;
Expand Down
39 changes: 39 additions & 0 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
13 changes: 13 additions & 0 deletions packages/editor-ui/src/api/cloudPlans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Cloud, IRestApiContext, InstanceUsage } from '@/Interface';
import { get } from '@/utils';

export async function getCurrentPlan(
context: IRestApiContext,
cloudUserId: string,
): Promise<Cloud.PlanData> {
return get(context.baseUrl, `/user/${cloudUserId}/plan`);
}

export async function getCurrentUsage(context: IRestApiContext): Promise<InstanceUsage> {
return get(context.baseUrl, '/limits');
}
Loading

0 comments on commit cd7c312

Please sign in to comment.