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 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 { sortByQueryMiddleware } from '@/middlewares/list-query/sort-by';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
@@ -24,6 +24,7 @@ export class InsightsController {
|
||||
|
||||
@Get('/by-workflow', { middlewares: [paginationListQueryMiddleware, sortByQueryMiddleware] })
|
||||
@GlobalScope('insights:list')
|
||||
@Licensed('feat:insights:viewDashboard')
|
||||
async getInsightsByWorkflow(
|
||||
_req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@@ -39,6 +40,7 @@ export class InsightsController {
|
||||
|
||||
@Get('/by-time')
|
||||
@GlobalScope('insights:list')
|
||||
@Licensed('feat:insights:viewDashboard')
|
||||
async getInsightsByTime(): Promise<InsightsByTime[]> {
|
||||
return await this.insightsService.getInsightsByTime({
|
||||
maxAgeInDays: this.maxAgeInDaysFilteredInsights,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
@@ -6,44 +5,58 @@ import { createUser } from '../shared/db/users';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import * as utils from '../shared/utils';
|
||||
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
let owner: User;
|
||||
let admin: User;
|
||||
let member: User;
|
||||
mockInstance(Telemetry);
|
||||
|
||||
let agents: Record<string, SuperAgentTest> = {};
|
||||
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['insights', 'license', 'auth'],
|
||||
enabledFeatures: [],
|
||||
enabledFeatures: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({ role: 'global:owner' });
|
||||
admin = await createUser({ role: 'global:admin' });
|
||||
member = await createUser({ role: 'global:member' });
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
agents.owner = authOwnerAgent;
|
||||
const owner = await createUser({ role: 'global:owner' });
|
||||
const admin = await createUser({ role: 'global:admin' });
|
||||
const member = await createUser({ role: 'global:member' });
|
||||
agents.owner = testServer.authAgentFor(owner);
|
||||
agents.admin = testServer.authAgentFor(admin);
|
||||
agents.member = testServer.authAgentFor(member);
|
||||
});
|
||||
|
||||
describe('GET /insights routes work for owner and admins', () => {
|
||||
test.each(['owner', 'member', 'admin'])(
|
||||
describe('GET /insights routes work for owner and admins for server with dashboard license', () => {
|
||||
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 === 'member' ? 403 : 200);
|
||||
await authAgent.get('/insights/by-time').expect(agentName === 'member' ? 403 : 200);
|
||||
await authAgent.get('/insights/by-workflow').expect(agentName === 'member' ? 403 : 200);
|
||||
await authAgent.get('/insights/summary').expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent.get('/insights/by-time').expect(agentName.includes('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 () => {
|
||||
await authOwnerAgent
|
||||
await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({ skip: '10', take: '20', sortBy: 'total:desc' })
|
||||
.expect(200);
|
||||
@@ -61,12 +74,12 @@ describe('GET /insights/by-worklow', () => {
|
||||
])(
|
||||
'Call should return internal server error with invalid pagination query parameters',
|
||||
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 () => {
|
||||
await authOwnerAgent
|
||||
await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({
|
||||
skip: '1',
|
||||
|
||||
@@ -1471,7 +1471,8 @@ export type CloudUpdateLinkSourceType =
|
||||
| 'worker-view'
|
||||
| 'external-secrets'
|
||||
| 'rbac'
|
||||
| 'debug';
|
||||
| 'debug'
|
||||
| 'insights';
|
||||
|
||||
export type UTMCampaign =
|
||||
| 'upgrade-custom-data-filter'
|
||||
@@ -1494,7 +1495,8 @@ export type UTMCampaign =
|
||||
| 'upgrade-worker-view'
|
||||
| 'upgrade-external-secrets'
|
||||
| 'upgrade-rbac'
|
||||
| 'upgrade-debug';
|
||||
| 'upgrade-debug'
|
||||
| 'upgrade-insights';
|
||||
|
||||
export type N8nBanners = {
|
||||
[key in BannerName]: {
|
||||
|
||||
@@ -80,8 +80,10 @@ watch(
|
||||
void insightsStore.summary.execute();
|
||||
}
|
||||
|
||||
void insightsStore.charts.execute();
|
||||
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
||||
if (insightsStore.isDashboardEnabled) {
|
||||
void insightsStore.charts.execute();
|
||||
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
@@ -102,41 +104,46 @@ watch(
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:class="$style.insightsBanner"
|
||||
/>
|
||||
<div v-if="insightsStore.isInsightsEnabled" :class="$style.insightsContent">
|
||||
<div :class="$style.insightsChartWrapper">
|
||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C11.6293 1 12.245 1.05813 12.8421 1.16931"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ i18n.baseText('insights.chart.loading') }}
|
||||
<div :class="$style.insightsContent">
|
||||
<InsightsPaywall
|
||||
v-if="!insightsStore.isDashboardEnabled"
|
||||
data-test-id="insights-dashboard-unlicensed"
|
||||
/>
|
||||
<div v-else>
|
||||
<div :class="$style.insightsChartWrapper">
|
||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C11.6293 1 12.245 1.05813 12.8421 1.16931"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ i18n.baseText('insights.chart.loading') }}
|
||||
</div>
|
||||
<component
|
||||
:is="chartComponents[props.insightType]"
|
||||
v-else
|
||||
:type="props.insightType"
|
||||
:data="insightsStore.charts.state"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.insightsTableWrapper">
|
||||
<InsightsTableWorkflows
|
||||
v-model:sort-by="sortTableBy"
|
||||
:data="insightsStore.table.state"
|
||||
:loading="insightsStore.table.isLoading"
|
||||
@update:options="fetchPaginatedTableData"
|
||||
/>
|
||||
</div>
|
||||
<component
|
||||
:is="chartComponents[props.insightType]"
|
||||
v-else
|
||||
:type="props.insightType"
|
||||
:data="insightsStore.charts.state"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.insightsTableWrapper">
|
||||
<InsightsTableWorkflows
|
||||
v-model:sort-by="sortTableBy"
|
||||
:data="insightsStore.table.state"
|
||||
:loading="insightsStore.table.isLoading"
|
||||
@update:options="fetchPaginatedTableData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InsightsPaywall v-else data-test-id="insights-dashboard-unlicensed" />
|
||||
</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>
|
||||
<div :class="$style.callout">
|
||||
<N8nIcon icon="lock" size="size"></N8nIcon>
|
||||
<N8nText bold tag="h3" size="large">Upgrade to Pro or Enterprise to see full data</N8nText>
|
||||
<N8nText
|
||||
>Gain access to detailed execution data with one year data retention.
|
||||
<N8nLink to="/">Learn more</N8nLink>
|
||||
<N8nIcon icon="lock" size="xlarge"></N8nIcon>
|
||||
<N8nText bold tag="h3" size="large">
|
||||
{{ i18n.baseText('insights.dashboard.paywall.title') }}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +30,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 6rem 0;
|
||||
align-items: center;
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
);
|
||||
|
||||
const isInsightsEnabled = computed(() => settingsStore.settings.insights.enabled);
|
||||
const isDashboardEnabled = computed(() => settingsStore.settings.insights.dashboard);
|
||||
|
||||
const isSummaryEnabled = computed(
|
||||
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
||||
@@ -56,6 +57,7 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
globalInsightsPermissions,
|
||||
isInsightsEnabled,
|
||||
isSummaryEnabled,
|
||||
isDashboardEnabled,
|
||||
summary,
|
||||
charts,
|
||||
table,
|
||||
|
||||
@@ -3092,6 +3092,9 @@
|
||||
"insights.dashboard.table.projectName": "Project name",
|
||||
"insights.dashboard.table.estimate": "Estimate",
|
||||
"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.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
||||
"insights.chart.failed": "Failed",
|
||||
|
||||
Reference in New Issue
Block a user