mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(editor): Add cloud ExecutionsUsage and API blocking using licenses (#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>
This commit is contained in:
@@ -132,6 +132,10 @@ export class License {
|
|||||||
return this.isFeatureEnabled(LICENSE_FEATURES.VERSION_CONTROL);
|
return this.isFeatureEnabled(LICENSE_FEATURES.VERSION_CONTROL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAPIDisabled() {
|
||||||
|
return this.isFeatureEnabled(LICENSE_FEATURES.API_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentEntitlements() {
|
getCurrentEntitlements() {
|
||||||
return this.manager?.getCurrentEntitlements() ?? [];
|
return this.manager?.getCurrentEntitlements() ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import * as Db from '@/Db';
|
|||||||
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
|
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import { License } from '@/License';
|
||||||
|
|
||||||
async function createApiRouter(
|
async function createApiRouter(
|
||||||
version: string,
|
version: string,
|
||||||
@@ -151,3 +152,12 @@ export const loadPublicApiVersions = async (
|
|||||||
apiLatestVersion: Number(versions.pop()?.charAt(1)) ?? 1,
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ import {
|
|||||||
|
|
||||||
import { executionsController } from '@/executions/executions.controller';
|
import { executionsController } from '@/executions/executions.controller';
|
||||||
import { workflowStatsController } from '@/api/workflowStats.api';
|
import { workflowStatsController } from '@/api/workflowStats.api';
|
||||||
import { loadPublicApiVersions } from '@/PublicApi';
|
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
|
||||||
import {
|
import {
|
||||||
getInstanceBaseUrl,
|
getInstanceBaseUrl,
|
||||||
isEmailSetUp,
|
isEmailSetUp,
|
||||||
@@ -277,7 +277,7 @@ export class Server extends AbstractServer {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
publicApi: {
|
publicApi: {
|
||||||
enabled: !config.getEnv('publicApi.disabled'),
|
enabled: isApiEnabled(),
|
||||||
latestVersion: 1,
|
latestVersion: 1,
|
||||||
path: config.getEnv('publicApi.path'),
|
path: config.getEnv('publicApi.path'),
|
||||||
swaggerUi: {
|
swaggerUi: {
|
||||||
@@ -538,7 +538,7 @@ export class Server extends AbstractServer {
|
|||||||
this.endpointWebhook,
|
this.endpointWebhook,
|
||||||
this.endpointWebhookTest,
|
this.endpointWebhookTest,
|
||||||
this.endpointPresetCredentials,
|
this.endpointPresetCredentials,
|
||||||
config.getEnv('publicApi.disabled') ? publicApiEndpoint : '',
|
isApiEnabled() ? '' : publicApiEndpoint,
|
||||||
...excludeEndpoints.split(':'),
|
...excludeEndpoints.split(':'),
|
||||||
].filter((u) => !!u);
|
].filter((u) => !!u);
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ export class Server extends AbstractServer {
|
|||||||
// Public API
|
// Public API
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
if (!config.getEnv('publicApi.disabled')) {
|
if (isApiEnabled()) {
|
||||||
const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(publicApiEndpoint);
|
const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(publicApiEndpoint);
|
||||||
this.app.use(...apiRouters);
|
this.app.use(...apiRouters);
|
||||||
this.frontendSettings.publicApi.latestVersion = apiLatestVersion;
|
this.frontendSettings.publicApi.latestVersion = apiLatestVersion;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { getN8nPackageJson, inDevelopment } from '@/constants';
|
import { getN8nPackageJson, inDevelopment } from '@/constants';
|
||||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import type { Risk, n8n } from '@/audit/types';
|
import type { Risk, n8n } from '@/audit/types';
|
||||||
|
import { isApiEnabled } from '@/PublicApi';
|
||||||
|
|
||||||
function getSecuritySettings() {
|
function getSecuritySettings() {
|
||||||
if (config.getEnv('deployment.type') === 'cloud') return null;
|
if (config.getEnv('deployment.type') === 'cloud') return null;
|
||||||
@@ -34,7 +35,7 @@ function getSecuritySettings() {
|
|||||||
communityPackagesEnabled: config.getEnv('nodes.communityPackages.enabled'),
|
communityPackagesEnabled: config.getEnv('nodes.communityPackages.enabled'),
|
||||||
versionNotificationsEnabled: config.getEnv('versionNotifications.enabled'),
|
versionNotificationsEnabled: config.getEnv('versionNotifications.enabled'),
|
||||||
templatesEnabled: config.getEnv('templates.enabled'),
|
templatesEnabled: config.getEnv('templates.enabled'),
|
||||||
publicApiEnabled: !config.getEnv('publicApi.disabled'),
|
publicApiEnabled: isApiEnabled(),
|
||||||
userManagementEnabled,
|
userManagementEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const enum LICENSE_FEATURES {
|
|||||||
ADVANCED_EXECUTION_FILTERS = 'feat:advancedExecutionFilters',
|
ADVANCED_EXECUTION_FILTERS = 'feat:advancedExecutionFilters',
|
||||||
VARIABLES = 'feat:variables',
|
VARIABLES = 'feat:variables',
|
||||||
VERSION_CONTROL = 'feat:versionControl',
|
VERSION_CONTROL = 'feat:versionControl',
|
||||||
|
API_DISABLED = 'feat:apiDisabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum LICENSE_QUOTAS {
|
export const enum LICENSE_QUOTAS {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'medium',
|
default: 'medium',
|
||||||
validator: (value: string): boolean =>
|
validator: (value: string): boolean =>
|
||||||
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
|
['xmini', 'mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -278,6 +278,17 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
|
|||||||
* Sizes
|
* 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 {
|
.mini {
|
||||||
--button-padding-vertical: var(--spacing-4xs);
|
--button-padding-vertical: var(--spacing-4xs);
|
||||||
--button-padding-horizontal: var(--spacing-2xs);
|
--button-padding-horizontal: var(--spacing-2xs);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.lowerContent, 'pb-2xs']">
|
<div :class="[$style.lowerContent, 'pb-2xs']">
|
||||||
|
<slot name="beforeLowerMenu"></slot>
|
||||||
<el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
|
<el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
|
||||||
<n8n-menu-item
|
<n8n-menu-item
|
||||||
v-for="item in lowerMenuItems"
|
v-for="item in lowerMenuItems"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
import Modals from '@/components/Modals.vue';
|
import Modals from '@/components/Modals.vue';
|
||||||
import LoadingView from '@/views/LoadingView.vue';
|
import LoadingView from '@/views/LoadingView.vue';
|
||||||
import Telemetry from '@/components/Telemetry.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 { userHelpers } from '@/mixins/userHelpers';
|
||||||
import { loadLanguage } from '@/plugins/i18n';
|
import { loadLanguage } from '@/plugins/i18n';
|
||||||
@@ -42,10 +42,12 @@ import { useUsersStore } from '@/stores/users.store';
|
|||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useCloudPlanStore } from './stores/cloudPlan.store';
|
||||||
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
||||||
import { newVersions } from '@/mixins/newVersions';
|
import { newVersions } from '@/mixins/newVersions';
|
||||||
import { useRoute } from 'vue-router/composables';
|
import { useRoute } from 'vue-router/composables';
|
||||||
import { useVersionControlStore } from '@/stores/versionControl.store';
|
import { useVersionControlStore } from '@/stores/versionControl.store';
|
||||||
|
import { useUsageStore } from '@/stores/usage.store';
|
||||||
import { useExternalHooks } from '@/composables';
|
import { useExternalHooks } from '@/composables';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export default defineComponent({
|
|||||||
useUIStore,
|
useUIStore,
|
||||||
useUsersStore,
|
useUsersStore,
|
||||||
useVersionControlStore,
|
useVersionControlStore,
|
||||||
|
useCloudPlanStore,
|
||||||
|
useUsageStore,
|
||||||
),
|
),
|
||||||
defaultLocale(): string {
|
defaultLocale(): string {
|
||||||
return this.rootStore.defaultLocale;
|
return this.rootStore.defaultLocale;
|
||||||
@@ -185,6 +189,25 @@ export default defineComponent({
|
|||||||
window.document.body.classList.add(`theme-${theme}`);
|
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() {
|
async mounted() {
|
||||||
this.setTheme();
|
this.setTheme();
|
||||||
@@ -193,6 +216,7 @@ export default defineComponent({
|
|||||||
this.authenticate();
|
this.authenticate();
|
||||||
this.redirectIfNecessary();
|
this.redirectIfNecessary();
|
||||||
void this.checkForNewVersions();
|
void this.checkForNewVersions();
|
||||||
|
void this.checkForCloudPlanData();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1450,3 +1450,42 @@ export type VersionControlPreferences = {
|
|||||||
branchColor: string;
|
branchColor: string;
|
||||||
publicKey?: 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
packages/editor-ui/src/api/cloudPlans.ts
Normal file
13
packages/editor-ui/src/api/cloudPlans.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
203
packages/editor-ui/src/components/ExecutionsUsage.vue
Normal file
203
packages/editor-ui/src/components/ExecutionsUsage.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div v-if="isTrialExpired" :class="$style.usageText">
|
||||||
|
<n8n-text size="xsmall" color="danger">
|
||||||
|
{{ locale.baseText('executionUsage.expired.text') }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!isTrialExpired && trialHasExecutionsLeft" :class="$style.usageText">
|
||||||
|
<i18n path="executionUsage.currentUsage">
|
||||||
|
<template #text>
|
||||||
|
<n8n-text size="small" color="text-dark">
|
||||||
|
{{ locale.baseText('executionUsage.currentUsage.text') }}
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
<template #count>
|
||||||
|
<n8n-text size="small" :bold="true" color="warning">
|
||||||
|
{{
|
||||||
|
locale.baseText('executionUsage.currentUsage.count', {
|
||||||
|
adjustToNumber: daysLeftOnTrial,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!trialHasExecutionsLeft" :class="$style.usageText">
|
||||||
|
<n8n-text size="xsmall">
|
||||||
|
{{ locale.baseText('executionUsage.ranOutOfExecutions.text') }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isTrialExpired" :class="$style.usageCounter">
|
||||||
|
<div :class="$style.progressBarDiv">
|
||||||
|
<progress
|
||||||
|
:class="[
|
||||||
|
trialHasExecutionsLeft ? $style.progressBarSuccess : $style.progressBarDanger,
|
||||||
|
$style.progressBar,
|
||||||
|
]"
|
||||||
|
:value="currentExecutionsWithThreshold"
|
||||||
|
:max="maxExecutions"
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.executionsCountSection">
|
||||||
|
<n8n-text size="xsmall" :color="trialHasExecutionsLeft ? 'text-dark' : 'danger'">
|
||||||
|
{{ currentExecutions }}/{{ maxExecutions }}
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-text size="xsmall" :color="trialHasExecutionsLeft ? 'text-dark' : 'danger'">{{
|
||||||
|
locale.baseText('executionUsage.label.executions')
|
||||||
|
}}</n8n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.upgradeButtonSection">
|
||||||
|
<n8n-button
|
||||||
|
:label="locale.baseText('executionUsage.button.upgrade')"
|
||||||
|
size="xmini"
|
||||||
|
icon="gem"
|
||||||
|
type="success"
|
||||||
|
:block="true"
|
||||||
|
@click="onUpgradeClicked"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import type { CloudPlanAndUsageData } from '@/Interface';
|
||||||
|
import { CLOUD_CHANGE_PLAN_PAGE } from '@/constants';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const PROGRESS_BAR_MINIMUM_THRESHOLD = 8;
|
||||||
|
|
||||||
|
const props = defineProps<{ cloudPlanData: CloudPlanAndUsageData | null }>();
|
||||||
|
|
||||||
|
const now = DateTime.utc();
|
||||||
|
|
||||||
|
const daysLeftOnTrial = computed(() => {
|
||||||
|
const { days = 0 } = getPlanExpirationDate().diff(now, ['days']).toObject();
|
||||||
|
return Math.ceil(days);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTrialExpired = computed(() => {
|
||||||
|
if (!props.cloudPlanData?.expirationDate) return false;
|
||||||
|
const trialEndsAt = DateTime.fromISO(props.cloudPlanData.expirationDate);
|
||||||
|
return now.toMillis() > trialEndsAt.toMillis();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlanExpirationDate = () => DateTime.fromISO(props?.cloudPlanData?.expirationDate ?? '');
|
||||||
|
|
||||||
|
const trialHasExecutionsLeft = computed(() => {
|
||||||
|
if (!props.cloudPlanData?.usage) return 0;
|
||||||
|
return props.cloudPlanData.usage.executions < props.cloudPlanData.monthlyExecutionsLimit;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentExecutions = computed(() => {
|
||||||
|
if (!props.cloudPlanData?.usage) return 0;
|
||||||
|
const usedExecutions = props.cloudPlanData.usage.executions;
|
||||||
|
const executionsQuota = props.cloudPlanData.monthlyExecutionsLimit;
|
||||||
|
return usedExecutions > executionsQuota ? executionsQuota : usedExecutions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentExecutionsWithThreshold = computed(() => {
|
||||||
|
if (!props.cloudPlanData?.usage) return 0;
|
||||||
|
const usedExecutions = props.cloudPlanData.usage.executions;
|
||||||
|
const executionsQuota = props.cloudPlanData.monthlyExecutionsLimit;
|
||||||
|
const threshold = (PROGRESS_BAR_MINIMUM_THRESHOLD * executionsQuota) / 100;
|
||||||
|
return usedExecutions < threshold ? threshold : usedExecutions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxExecutions = computed(() => {
|
||||||
|
if (!props.cloudPlanData?.monthlyExecutionsLimit) return 0;
|
||||||
|
return props.cloudPlanData.monthlyExecutionsLimit;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpgradeClicked = () => {
|
||||||
|
location.href = CLOUD_CHANGE_PLAN_PAGE;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border: var(--border-base);
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDiv {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
.progressBar::-webkit-progress-bar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
.progressBar::-moz-progress-bar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarSuccess::-moz-progress-bar {
|
||||||
|
background: var(--color-foreground-xdark);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarSuccess::-webkit-progress-value {
|
||||||
|
background: var(--color-foreground-xdark);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDanger::-webkit-progress-value {
|
||||||
|
background: var(--color-danger);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDanger::-moz-progress-bar {
|
||||||
|
background: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageText {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
line-height: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageCounter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
font-size: var(--font-size-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.executionsCountSection {
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgradeButtonSection {
|
||||||
|
margin: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,6 +25,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #beforeLowerMenu>
|
||||||
|
<ExecutionsUsage
|
||||||
|
:cloud-plan-data="currentPlanAndUsageData"
|
||||||
|
v-if="!isCollapsed && userIsTrialing"
|
||||||
|
/></template>
|
||||||
<template #menuSuffix>
|
<template #menuSuffix>
|
||||||
<div v-if="hasVersionUpdates || versionControlStore.state.currentBranch">
|
<div v-if="hasVersionUpdates || versionControlStore.state.currentBranch">
|
||||||
<div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel">
|
<div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel">
|
||||||
@@ -110,7 +116,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
|
import type { CloudPlanAndUsageData, IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
|
||||||
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
||||||
|
|
||||||
@@ -132,11 +138,14 @@ import { useRootStore } from '@/stores/n8nRoot.store';
|
|||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { isNavigationFailure } from 'vue-router';
|
import { isNavigationFailure } from 'vue-router';
|
||||||
import { useVersionControlStore } from '@/stores/versionControl.store';
|
import { useVersionControlStore } from '@/stores/versionControl.store';
|
||||||
|
import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
|
||||||
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'MainSidebar',
|
name: 'MainSidebar',
|
||||||
components: {
|
components: {
|
||||||
GiftNotificationIcon,
|
GiftNotificationIcon,
|
||||||
|
ExecutionsUsage,
|
||||||
},
|
},
|
||||||
mixins: [genericHelpers, workflowHelpers, workflowRun, userHelpers, debounceHelper],
|
mixins: [genericHelpers, workflowHelpers, workflowRun, userHelpers, debounceHelper],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
@@ -147,7 +156,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// @ts-ignore
|
|
||||||
basePath: '',
|
basePath: '',
|
||||||
fullyExpanded: false,
|
fullyExpanded: false,
|
||||||
};
|
};
|
||||||
@@ -161,6 +169,7 @@ export default defineComponent({
|
|||||||
useVersionsStore,
|
useVersionsStore,
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
useVersionControlStore,
|
useVersionControlStore,
|
||||||
|
useCloudPlanStore,
|
||||||
),
|
),
|
||||||
currentBranch(): string {
|
currentBranch(): string {
|
||||||
return this.versionControlStore.state.currentBranch;
|
return this.versionControlStore.state.currentBranch;
|
||||||
@@ -323,6 +332,18 @@ export default defineComponent({
|
|||||||
];
|
];
|
||||||
return [...items, ...regularItems];
|
return [...items, ...regularItems];
|
||||||
},
|
},
|
||||||
|
userIsTrialing(): boolean {
|
||||||
|
return this.cloudPlanStore.userIsTrialing;
|
||||||
|
},
|
||||||
|
currentPlanAndUsageData(): CloudPlanAndUsageData | null {
|
||||||
|
const planData = this.cloudPlanStore.currentPlanData;
|
||||||
|
const usage = this.cloudPlanStore.currentUsageData;
|
||||||
|
if (!planData || !usage) return null;
|
||||||
|
return {
|
||||||
|
...planData,
|
||||||
|
usage,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.basePath = this.rootStore.baseUrl;
|
this.basePath = this.rootStore.baseUrl;
|
||||||
|
|||||||
@@ -551,3 +551,13 @@ export const ALLOWED_HTML_TAGS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const GITHUB_STARS_BANNER_SHOW_UNTIL_DATE = new Date('2023-06-01');
|
export const GITHUB_STARS_BANNER_SHOW_UNTIL_DATE = new Date('2023-06-01');
|
||||||
|
|
||||||
|
export const CLOUD_CHANGE_PLAN_PAGE = window.location.host.includes('stage-app.n8n.cloud')
|
||||||
|
? 'https://stage-app.n8n.cloud/account/change-plan'
|
||||||
|
: 'https://app.n8n.cloud/account/change-plan';
|
||||||
|
|
||||||
|
export const CLOUD_BASE_URL_STAGING = 'https://stage-api.n8n.cloud';
|
||||||
|
|
||||||
|
export const CLOUD_BASE_URL_PRODUCTION = 'https://api.n8n.cloud';
|
||||||
|
|
||||||
|
export const CLOUD_TRIAL_CHECK_INTERVAL = 5000;
|
||||||
|
|||||||
@@ -1034,6 +1034,8 @@
|
|||||||
"pushConnection.pollingNode.dataNotFound.message": "We didn’t find any data in {service} to simulate an event. Please create one in {service} and try again.",
|
"pushConnection.pollingNode.dataNotFound.message": "We didn’t find any data in {service} to simulate an event. Please create one in {service} and try again.",
|
||||||
"pushConnection.executionFailed": "Execution failed",
|
"pushConnection.executionFailed": "Execution failed",
|
||||||
"pushConnection.executionFailed.message": "There might not be enough memory to finish the execution. Tips for avoiding this <a target=\"_blank\" href=\"https://docs.n8n.io/flow-logic/error-handling/memory-errors/\">here</a>",
|
"pushConnection.executionFailed.message": "There might not be enough memory to finish the execution. Tips for avoiding this <a target=\"_blank\" href=\"https://docs.n8n.io/flow-logic/error-handling/memory-errors/\">here</a>",
|
||||||
|
"pushConnection.executionError": "There was a problem executing the workflow{error}",
|
||||||
|
"pushConnection.executionError.details": "<br /><strong>{details}</strong>",
|
||||||
"resourceLocator.id.placeholder": "Enter ID...",
|
"resourceLocator.id.placeholder": "Enter ID...",
|
||||||
"resourceLocator.mode.id": "By ID",
|
"resourceLocator.mode.id": "By ID",
|
||||||
"resourceLocator.mode.url": "By URL",
|
"resourceLocator.mode.url": "By URL",
|
||||||
@@ -1245,6 +1247,9 @@
|
|||||||
"settings.log-streaming.destinationDelete.message": "Are you sure that you want to delete '{destinationName}'?",
|
"settings.log-streaming.destinationDelete.message": "Are you sure that you want to delete '{destinationName}'?",
|
||||||
"settings.log-streaming.addDestination": "Add new destination",
|
"settings.log-streaming.addDestination": "Add new destination",
|
||||||
"settings.log-streaming.destinations": "Log destinations",
|
"settings.log-streaming.destinations": "Log destinations",
|
||||||
|
"settings.api.trial.upgradePlan.title": "Upgrade to use API",
|
||||||
|
"settings.api.trial.upgradePlan.description": "To prevent abuse, we limit API access to your workspace during your trial. If this is hindering your evaluation of n8n, please contact <a href=\"mailto:support@n8n.io\">support@n8n.io</a>",
|
||||||
|
"settings.api.trial.upgradePlan.cta": "Upgrade plan",
|
||||||
"settings.api.create.description": "Control n8n programmatically using the <a href=\"https://docs.n8n.io/api\" target=\"_blank\">n8n API</a>",
|
"settings.api.create.description": "Control n8n programmatically using the <a href=\"https://docs.n8n.io/api\" target=\"_blank\">n8n API</a>",
|
||||||
"settings.api.create.button": "Create an API Key",
|
"settings.api.create.button": "Create an API Key",
|
||||||
"settings.api.create.button.loading": "Creating API Key...",
|
"settings.api.create.button.loading": "Creating API Key...",
|
||||||
@@ -1822,6 +1827,11 @@
|
|||||||
"userActivationSurveyModal.sharedFeedback.error": "Problem sharing feedback, try again",
|
"userActivationSurveyModal.sharedFeedback.error": "Problem sharing feedback, try again",
|
||||||
"sso.login.divider": "or",
|
"sso.login.divider": "or",
|
||||||
"sso.login.button": "Continue with SSO",
|
"sso.login.button": "Continue with SSO",
|
||||||
"pushConnection.executionError": "There was a problem executing the workflow{error}",
|
"executionUsage.currentUsage": "{text} {count}",
|
||||||
"pushConnection.executionError.details": "<br /><strong>{details}</strong>"
|
"executionUsage.currentUsage.text": "You are in a free trial with limited executions. You have",
|
||||||
|
"executionUsage.currentUsage.count": "{n} day left. | {n} days left.",
|
||||||
|
"executionUsage.label.executions": "Executions",
|
||||||
|
"executionUsage.button.upgrade": "Upgrade plan",
|
||||||
|
"executionUsage.expired.text": "You're trial is over. Upgrade now to keep your automation data",
|
||||||
|
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ import {
|
|||||||
faTree,
|
faTree,
|
||||||
faStickyNote as faSolidStickyNote,
|
faStickyNote as faSolidStickyNote,
|
||||||
faUserLock,
|
faUserLock,
|
||||||
|
faGem,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faVariable } from './custom';
|
import { faVariable } from './custom';
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||||
@@ -266,5 +267,6 @@ addIcon(faVariable);
|
|||||||
addIcon(faVideo);
|
addIcon(faVideo);
|
||||||
addIcon(faTree);
|
addIcon(faTree);
|
||||||
addIcon(faUserLock);
|
addIcon(faUserLock);
|
||||||
|
addIcon(faGem);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|||||||
83
packages/editor-ui/src/stores/cloudPlan.store.ts
Normal file
83
packages/editor-ui/src/stores/cloudPlan.store.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import type { CloudPlanState } from '@/Interface';
|
||||||
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
const DEFAULT_STATE: CloudPlanState = {
|
||||||
|
data: null,
|
||||||
|
usage: null,
|
||||||
|
loadingPlan: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
const state = reactive<CloudPlanState>(DEFAULT_STATE);
|
||||||
|
|
||||||
|
const setData = (data: CloudPlanState['data']) => {
|
||||||
|
state.data = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUsage = (data: CloudPlanState['usage']) => {
|
||||||
|
state.usage = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIsTrialing = computed(() => state.data?.metadata?.group === 'trial');
|
||||||
|
|
||||||
|
const currentPlanData = computed(() => state.data);
|
||||||
|
|
||||||
|
const currentUsageData = computed(() => state.usage);
|
||||||
|
|
||||||
|
const trialExpired = computed(
|
||||||
|
() =>
|
||||||
|
state.data?.metadata?.group === 'trial' &&
|
||||||
|
DateTime.now().toMillis() >= DateTime.fromISO(state.data?.expirationDate).toMillis(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allExecutionsUsed = computed(() => {
|
||||||
|
if (!state.usage?.executions || !state.data?.monthlyExecutionsLimit) return false;
|
||||||
|
return state.usage?.executions >= state.data?.monthlyExecutionsLimit;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOwnerCurrentPLan = async () => {
|
||||||
|
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
|
||||||
|
const hasCloudPlan =
|
||||||
|
usersStore.currentUser?.isOwner && settingsStore.isCloudDeployment && cloudUserId;
|
||||||
|
if (!hasCloudPlan) throw new Error('User does not have a cloud plan');
|
||||||
|
state.loadingPlan = true;
|
||||||
|
let plan;
|
||||||
|
try {
|
||||||
|
plan = await getCurrentPlan(rootStore.getRestCloudApiContext, `${cloudUserId}`);
|
||||||
|
state.data = plan;
|
||||||
|
state.loadingPlan = false;
|
||||||
|
} catch (error) {
|
||||||
|
state.loadingPlan = false;
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstanceCurrentUsage = async () => {
|
||||||
|
const usage = await getCurrentUsage({ baseUrl: rootStore.getBaseUrl, sessionId: '' });
|
||||||
|
state.usage = usage;
|
||||||
|
return usage;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
getOwnerCurrentPLan,
|
||||||
|
getInstanceCurrentUsage,
|
||||||
|
userIsTrialing,
|
||||||
|
currentPlanData,
|
||||||
|
currentUsageData,
|
||||||
|
trialExpired,
|
||||||
|
allExecutionsUsed,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -21,3 +21,4 @@ export * from './versions.store';
|
|||||||
export * from './webhooks.store';
|
export * from './webhooks.store';
|
||||||
export * from './workflows.ee.store';
|
export * from './workflows.ee.store';
|
||||||
export * from './workflows.store';
|
export * from './workflows.store';
|
||||||
|
export * from './cloudPlan.store';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { STORES } from '@/constants';
|
import { CLOUD_BASE_URL_PRODUCTION, CLOUD_BASE_URL_STAGING, STORES } from '@/constants';
|
||||||
import type { IRestApiContext, RootState } from '@/Interface';
|
import type { IRestApiContext, RootState } from '@/Interface';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
@@ -48,6 +48,15 @@ export const useRootStore = defineStore(STORES.ROOT, {
|
|||||||
return `${this.baseUrl}${this.restEndpoint}`;
|
return `${this.baseUrl}${this.restEndpoint}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRestCloudApiContext(): IRestApiContext {
|
||||||
|
return {
|
||||||
|
baseUrl: window.location.host.includes('stage-app.n8n.cloud')
|
||||||
|
? CLOUD_BASE_URL_STAGING
|
||||||
|
: CLOUD_BASE_URL_PRODUCTION,
|
||||||
|
sessionId: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getRestApiContext(): IRestApiContext {
|
getRestApiContext(): IRestApiContext {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.getRestUrl,
|
baseUrl: this.getRestUrl,
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ async function request(config: {
|
|||||||
baseURL,
|
baseURL,
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
if (import.meta.env.NODE_ENV !== 'production' && !baseURL.includes('api.n8n.io')) {
|
if (
|
||||||
|
import.meta.env.NODE_ENV !== 'production' &&
|
||||||
|
!baseURL.includes('api.n8n.io') &&
|
||||||
|
!baseURL.includes('n8n.cloud')
|
||||||
|
) {
|
||||||
options.withCredentials = true;
|
options.withCredentials = true;
|
||||||
}
|
}
|
||||||
if (['POST', 'PATCH', 'PUT'].includes(method)) {
|
if (['POST', 'PATCH', 'PUT'].includes(method)) {
|
||||||
|
|||||||
@@ -62,7 +62,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n8n-action-box
|
<n8n-action-box
|
||||||
v-else-if="mounted"
|
v-else-if="isTrialing"
|
||||||
|
:heading="$locale.baseText('settings.api.trial.upgradePlan.title')"
|
||||||
|
:description="$locale.baseText('settings.api.trial.upgradePlan.description')"
|
||||||
|
:buttonText="$locale.baseText('settings.api.trial.upgradePlan.cta')"
|
||||||
|
@click="onUpgrade"
|
||||||
|
/>
|
||||||
|
<n8n-action-box
|
||||||
|
v-else-if="mounted && !isLoadingCloudPlans"
|
||||||
:buttonText="
|
:buttonText="
|
||||||
$locale.baseText(
|
$locale.baseText(
|
||||||
loading ? 'settings.api.create.button.loading' : 'settings.api.create.button',
|
loading ? 'settings.api.create.button.loading' : 'settings.api.create.button',
|
||||||
@@ -85,6 +92,8 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants';
|
import { DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants';
|
||||||
|
import { useCloudPlanStore } from '@/stores';
|
||||||
|
import { CLOUD_CHANGE_PLAN_PAGE } from '@/constants';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SettingsApiView',
|
name: 'SettingsApiView',
|
||||||
@@ -117,12 +126,21 @@ export default defineComponent({
|
|||||||
: `https://${DOCS_DOMAIN}/api/api-reference/`;
|
: `https://${DOCS_DOMAIN}/api/api-reference/`;
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useRootStore, useSettingsStore, useUsersStore),
|
...mapStores(useRootStore, useSettingsStore, useUsersStore, useCloudPlanStore),
|
||||||
currentUser(): IUser | null {
|
currentUser(): IUser | null {
|
||||||
return this.usersStore.currentUser;
|
return this.usersStore.currentUser;
|
||||||
},
|
},
|
||||||
|
isTrialing(): boolean {
|
||||||
|
return this.cloudPlanStore.userIsTrialing;
|
||||||
|
},
|
||||||
|
isLoadingCloudPlans(): boolean {
|
||||||
|
return this.cloudPlanStore.state.loadingPlan;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onUpgrade() {
|
||||||
|
location.href = CLOUD_CHANGE_PLAN_PAGE;
|
||||||
|
},
|
||||||
async showDeleteModal() {
|
async showDeleteModal() {
|
||||||
const confirmed = await this.confirm(
|
const confirmed = await this.confirm(
|
||||||
this.$locale.baseText('settings.api.delete.description'),
|
this.$locale.baseText('settings.api.delete.description'),
|
||||||
|
|||||||
Reference in New Issue
Block a user