mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Fix paywall for dashboard disabled licences (#14617)
This commit is contained in:
committed by
GitHub
parent
390c508946
commit
46d9b60049
@@ -1,7 +1,7 @@
|
|||||||
import { ListInsightsWorkflowQueryDto } from '@n8n/api-types';
|
import { ListInsightsWorkflowQueryDto } from '@n8n/api-types';
|
||||||
import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types';
|
import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types';
|
||||||
|
|
||||||
import { Get, GlobalScope, Query, RestController } from '@/decorators';
|
import { Get, GlobalScope, Licensed, Query, RestController } from '@/decorators';
|
||||||
import { paginationListQueryMiddleware } from '@/middlewares/list-query/pagination';
|
import { paginationListQueryMiddleware } from '@/middlewares/list-query/pagination';
|
||||||
import { sortByQueryMiddleware } from '@/middlewares/list-query/sort-by';
|
import { sortByQueryMiddleware } from '@/middlewares/list-query/sort-by';
|
||||||
import { AuthenticatedRequest } from '@/requests';
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
@@ -24,6 +24,7 @@ export class InsightsController {
|
|||||||
|
|
||||||
@Get('/by-workflow', { middlewares: [paginationListQueryMiddleware, sortByQueryMiddleware] })
|
@Get('/by-workflow', { middlewares: [paginationListQueryMiddleware, sortByQueryMiddleware] })
|
||||||
@GlobalScope('insights:list')
|
@GlobalScope('insights:list')
|
||||||
|
@Licensed('feat:insights:viewDashboard')
|
||||||
async getInsightsByWorkflow(
|
async getInsightsByWorkflow(
|
||||||
_req: AuthenticatedRequest,
|
_req: AuthenticatedRequest,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -39,6 +40,7 @@ export class InsightsController {
|
|||||||
|
|
||||||
@Get('/by-time')
|
@Get('/by-time')
|
||||||
@GlobalScope('insights:list')
|
@GlobalScope('insights:list')
|
||||||
|
@Licensed('feat:insights:viewDashboard')
|
||||||
async getInsightsByTime(): Promise<InsightsByTime[]> {
|
async getInsightsByTime(): Promise<InsightsByTime[]> {
|
||||||
return await this.insightsService.getInsightsByTime({
|
return await this.insightsService.getInsightsByTime({
|
||||||
maxAgeInDays: this.maxAgeInDaysFilteredInsights,
|
maxAgeInDays: this.maxAgeInDaysFilteredInsights,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { User } from '@/databases/entities/user';
|
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
@@ -6,44 +5,58 @@ import { createUser } from '../shared/db/users';
|
|||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
import * as utils from '../shared/utils';
|
import * as utils from '../shared/utils';
|
||||||
|
|
||||||
let authOwnerAgent: SuperAgentTest;
|
|
||||||
let owner: User;
|
|
||||||
let admin: User;
|
|
||||||
let member: User;
|
|
||||||
mockInstance(Telemetry);
|
mockInstance(Telemetry);
|
||||||
|
|
||||||
let agents: Record<string, SuperAgentTest> = {};
|
let agents: Record<string, SuperAgentTest> = {};
|
||||||
|
|
||||||
const testServer = utils.setupTestServer({
|
const testServer = utils.setupTestServer({
|
||||||
endpointGroups: ['insights', 'license', 'auth'],
|
endpointGroups: ['insights', 'license', 'auth'],
|
||||||
enabledFeatures: [],
|
enabledFeatures: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
owner = await createUser({ role: 'global:owner' });
|
const owner = await createUser({ role: 'global:owner' });
|
||||||
admin = await createUser({ role: 'global:admin' });
|
const admin = await createUser({ role: 'global:admin' });
|
||||||
member = await createUser({ role: 'global:member' });
|
const member = await createUser({ role: 'global:member' });
|
||||||
authOwnerAgent = testServer.authAgentFor(owner);
|
agents.owner = testServer.authAgentFor(owner);
|
||||||
agents.owner = authOwnerAgent;
|
|
||||||
agents.admin = testServer.authAgentFor(admin);
|
agents.admin = testServer.authAgentFor(admin);
|
||||||
agents.member = testServer.authAgentFor(member);
|
agents.member = testServer.authAgentFor(member);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /insights routes work for owner and admins', () => {
|
describe('GET /insights routes work for owner and admins for server with dashboard license', () => {
|
||||||
test.each(['owner', 'member', 'admin'])(
|
test.each(['owner', 'admin', 'member'])(
|
||||||
'Call should work and return empty summary for user %s',
|
'Call should work and return empty summary for user %s',
|
||||||
async (agentName: string) => {
|
async (agentName: string) => {
|
||||||
const authAgent = agents[agentName];
|
const authAgent = agents[agentName];
|
||||||
await authAgent.get('/insights/summary').expect(agentName === 'member' ? 403 : 200);
|
await authAgent.get('/insights/summary').expect(agentName.includes('member') ? 403 : 200);
|
||||||
await authAgent.get('/insights/by-time').expect(agentName === 'member' ? 403 : 200);
|
await authAgent.get('/insights/by-time').expect(agentName.includes('member') ? 403 : 200);
|
||||||
await authAgent.get('/insights/by-workflow').expect(agentName === 'member' ? 403 : 200);
|
await authAgent.get('/insights/by-workflow').expect(agentName.includes('member') ? 403 : 200);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /insights/by-worklow', () => {
|
describe('GET /insights routes return 403 for dashboard routes when summary license only', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
testServer.license.setDefaults({ features: ['feat:insights:viewSummary'] });
|
||||||
|
});
|
||||||
|
test.each(['owner', 'admin', 'member'])(
|
||||||
|
'Call should work and return empty summary for user %s',
|
||||||
|
async (agentName: string) => {
|
||||||
|
const authAgent = agents[agentName];
|
||||||
|
await authAgent.get('/insights/summary').expect(agentName.includes('member') ? 403 : 200);
|
||||||
|
await authAgent.get('/insights/by-time').expect(403);
|
||||||
|
await authAgent.get('/insights/by-workflow').expect(403);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /insights/by-workflow', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
testServer.license.setDefaults({
|
||||||
|
features: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||||
|
});
|
||||||
|
});
|
||||||
test('Call should work with valid query parameters', async () => {
|
test('Call should work with valid query parameters', async () => {
|
||||||
await authOwnerAgent
|
await agents.owner
|
||||||
.get('/insights/by-workflow')
|
.get('/insights/by-workflow')
|
||||||
.query({ skip: '10', take: '20', sortBy: 'total:desc' })
|
.query({ skip: '10', take: '20', sortBy: 'total:desc' })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -61,12 +74,12 @@ describe('GET /insights/by-worklow', () => {
|
|||||||
])(
|
])(
|
||||||
'Call should return internal server error with invalid pagination query parameters',
|
'Call should return internal server error with invalid pagination query parameters',
|
||||||
async (queryParams) => {
|
async (queryParams) => {
|
||||||
await authOwnerAgent.get('/insights/by-workflow').query(queryParams).expect(500);
|
await agents.owner.get('/insights/by-workflow').query(queryParams).expect(500);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Call should return bad request with invalid sortby query parameters', async () => {
|
test('Call should return bad request with invalid sortby query parameters', async () => {
|
||||||
await authOwnerAgent
|
await agents.owner
|
||||||
.get('/insights/by-workflow')
|
.get('/insights/by-workflow')
|
||||||
.query({
|
.query({
|
||||||
skip: '1',
|
skip: '1',
|
||||||
|
|||||||
@@ -1471,7 +1471,8 @@ export type CloudUpdateLinkSourceType =
|
|||||||
| 'worker-view'
|
| 'worker-view'
|
||||||
| 'external-secrets'
|
| 'external-secrets'
|
||||||
| 'rbac'
|
| 'rbac'
|
||||||
| 'debug';
|
| 'debug'
|
||||||
|
| 'insights';
|
||||||
|
|
||||||
export type UTMCampaign =
|
export type UTMCampaign =
|
||||||
| 'upgrade-custom-data-filter'
|
| 'upgrade-custom-data-filter'
|
||||||
@@ -1494,7 +1495,8 @@ export type UTMCampaign =
|
|||||||
| 'upgrade-worker-view'
|
| 'upgrade-worker-view'
|
||||||
| 'upgrade-external-secrets'
|
| 'upgrade-external-secrets'
|
||||||
| 'upgrade-rbac'
|
| 'upgrade-rbac'
|
||||||
| 'upgrade-debug';
|
| 'upgrade-debug'
|
||||||
|
| 'upgrade-insights';
|
||||||
|
|
||||||
export type N8nBanners = {
|
export type N8nBanners = {
|
||||||
[key in BannerName]: {
|
[key in BannerName]: {
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ watch(
|
|||||||
void insightsStore.summary.execute();
|
void insightsStore.summary.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (insightsStore.isDashboardEnabled) {
|
||||||
void insightsStore.charts.execute();
|
void insightsStore.charts.execute();
|
||||||
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@@ -102,7 +104,12 @@ watch(
|
|||||||
:loading="insightsStore.summary.isLoading"
|
:loading="insightsStore.summary.isLoading"
|
||||||
:class="$style.insightsBanner"
|
:class="$style.insightsBanner"
|
||||||
/>
|
/>
|
||||||
<div v-if="insightsStore.isInsightsEnabled" :class="$style.insightsContent">
|
<div :class="$style.insightsContent">
|
||||||
|
<InsightsPaywall
|
||||||
|
v-if="!insightsStore.isDashboardEnabled"
|
||||||
|
data-test-id="insights-dashboard-unlicensed"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
<div :class="$style.insightsChartWrapper">
|
<div :class="$style.insightsChartWrapper">
|
||||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||||
<svg
|
<svg
|
||||||
@@ -136,7 +143,7 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<InsightsPaywall v-else data-test-id="insights-dashboard-unlicensed" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
|
||||||
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const goToUpgrade = async () => {
|
||||||
|
await pageRedirectionHelper.goToUpgrade('insights', 'upgrade-insights');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.callout">
|
<div :class="$style.callout">
|
||||||
<N8nIcon icon="lock" size="size"></N8nIcon>
|
<N8nIcon icon="lock" size="xlarge"></N8nIcon>
|
||||||
<N8nText bold tag="h3" size="large">Upgrade to Pro or Enterprise to see full data</N8nText>
|
<N8nText bold tag="h3" size="large">
|
||||||
<N8nText
|
{{ i18n.baseText('insights.dashboard.paywall.title') }}
|
||||||
>Gain access to detailed execution data with one year data retention.
|
|
||||||
<N8nLink to="/">Learn more</N8nLink>
|
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nButton type="primary" size="large">Upgrade</N8nButton>
|
<N8nText>
|
||||||
|
{{ i18n.baseText('insights.dashboard.paywall.description') }}
|
||||||
|
</N8nText>
|
||||||
|
<N8nButton type="primary" native-type="button" size="large" @click="goToUpgrade">
|
||||||
|
{{ i18n.baseText('insights.dashboard.paywall.cta') }}
|
||||||
|
</N8nButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -17,6 +30,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 6rem 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isInsightsEnabled = computed(() => settingsStore.settings.insights.enabled);
|
const isInsightsEnabled = computed(() => settingsStore.settings.insights.enabled);
|
||||||
|
const isDashboardEnabled = computed(() => settingsStore.settings.insights.dashboard);
|
||||||
|
|
||||||
const isSummaryEnabled = computed(
|
const isSummaryEnabled = computed(
|
||||||
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
||||||
@@ -56,6 +57,7 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||||||
globalInsightsPermissions,
|
globalInsightsPermissions,
|
||||||
isInsightsEnabled,
|
isInsightsEnabled,
|
||||||
isSummaryEnabled,
|
isSummaryEnabled,
|
||||||
|
isDashboardEnabled,
|
||||||
summary,
|
summary,
|
||||||
charts,
|
charts,
|
||||||
table,
|
table,
|
||||||
|
|||||||
@@ -3092,6 +3092,9 @@
|
|||||||
"insights.dashboard.table.projectName": "Project name",
|
"insights.dashboard.table.projectName": "Project name",
|
||||||
"insights.dashboard.table.estimate": "Estimate",
|
"insights.dashboard.table.estimate": "Estimate",
|
||||||
"insights.dashboard.title": "Insights",
|
"insights.dashboard.title": "Insights",
|
||||||
|
"insights.dashboard.paywall.cta": "Upgrade",
|
||||||
|
"insights.dashboard.paywall.title": "Upgrade to access more detailed insights",
|
||||||
|
"insights.dashboard.paywall.description": "Gain access to more granular, per-workflow insights and visual breakdown of production executions over different time periods.",
|
||||||
"insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows",
|
"insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows",
|
||||||
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
||||||
"insights.chart.failed": "Failed",
|
"insights.chart.failed": "Failed",
|
||||||
|
|||||||
Reference in New Issue
Block a user