mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
fix(editor): Update frontend to handle unlicensed insights dashboard, if only Time saved feature is enabled (#17199)
This commit is contained in:
@@ -1,19 +1,75 @@
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import InsightsDashboard from './InsightsDashboard.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { defaultSettings } from '@/__tests__/defaults';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { within } from '@testing-library/vue';
|
||||
import { mockedStore, type MockedStore, useEmitters, waitAllPromises } from '@/__tests__/utils';
|
||||
import { within, screen, waitFor } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type {
|
||||
FrontendModuleSettings,
|
||||
InsightsByTime,
|
||||
InsightsByWorkflow,
|
||||
InsightsSummaryType,
|
||||
} from '@n8n/api-types';
|
||||
import { INSIGHT_TYPES } from '@/features/insights/insights.constants';
|
||||
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const renderComponent = createComponentRenderer(InsightsDashboard, {
|
||||
props: {
|
||||
insightType: 'total',
|
||||
const { emitters, addEmitter } = useEmitters<'n8nDataTableServer'>();
|
||||
|
||||
const mockRoute = reactive<{
|
||||
params: {
|
||||
insightType: InsightsSummaryType;
|
||||
};
|
||||
}>({
|
||||
params: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => mockRoute,
|
||||
useRouter: vi.fn(),
|
||||
RouterLink: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('vue-chartjs', () => ({
|
||||
Bar: {
|
||||
template: '<div>Bar</div>',
|
||||
},
|
||||
Line: {
|
||||
template: '<div>Line</div>',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||
const original = await importOriginal<object>();
|
||||
return {
|
||||
...original,
|
||||
N8nDataTableServer: defineComponent({
|
||||
props: {
|
||||
headers: { type: Array, required: true },
|
||||
items: { type: Array, required: true },
|
||||
itemsLength: { type: Number, required: true },
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
addEmitter('n8nDataTableServer', emit);
|
||||
},
|
||||
template: '<div data-test-id="insights-table"><slot /></div>',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const moduleSettings = {
|
||||
const mockTelemetry = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@/composables/useTelemetry', () => ({
|
||||
useTelemetry: () => mockTelemetry,
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(InsightsDashboard);
|
||||
|
||||
const moduleSettings: FrontendModuleSettings = {
|
||||
insights: {
|
||||
summary: true,
|
||||
dashboard: true,
|
||||
@@ -23,39 +79,436 @@ const moduleSettings = {
|
||||
licensed: true,
|
||||
granularity: 'hour',
|
||||
},
|
||||
{
|
||||
key: 'week',
|
||||
licensed: true,
|
||||
granularity: 'day',
|
||||
},
|
||||
{
|
||||
key: 'month',
|
||||
licensed: true,
|
||||
granularity: 'day',
|
||||
},
|
||||
{
|
||||
key: 'quarter',
|
||||
licensed: false,
|
||||
granularity: 'week',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mockSummaryData: InsightsSummaryDisplay = [
|
||||
{
|
||||
id: 'total',
|
||||
value: 1250,
|
||||
deviation: 15,
|
||||
unit: '',
|
||||
deviationUnit: '%',
|
||||
},
|
||||
{
|
||||
id: 'failed',
|
||||
value: 23,
|
||||
deviation: -8,
|
||||
unit: '',
|
||||
deviationUnit: '%',
|
||||
},
|
||||
{
|
||||
id: 'failureRate',
|
||||
value: 1.84,
|
||||
deviation: -0.5,
|
||||
unit: '%',
|
||||
deviationUnit: '%',
|
||||
},
|
||||
{
|
||||
id: 'timeSaved',
|
||||
value: 3600,
|
||||
deviation: 20,
|
||||
unit: 's',
|
||||
deviationUnit: '%',
|
||||
},
|
||||
{
|
||||
id: 'averageRunTime',
|
||||
value: 15,
|
||||
deviation: 2,
|
||||
unit: 's',
|
||||
deviationUnit: '%',
|
||||
},
|
||||
];
|
||||
|
||||
const mockChartsData: InsightsByTime[] = [
|
||||
{
|
||||
date: '2024-01-01',
|
||||
values: {
|
||||
total: 100,
|
||||
failed: 5,
|
||||
failureRate: 5,
|
||||
timeSaved: 45,
|
||||
averageRunTime: 12,
|
||||
succeeded: 95,
|
||||
},
|
||||
},
|
||||
{
|
||||
date: '2024-01-02',
|
||||
values: {
|
||||
total: 120,
|
||||
failed: 8,
|
||||
failureRate: 6.7,
|
||||
timeSaved: 55,
|
||||
averageRunTime: 15,
|
||||
succeeded: 112,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockTableData: InsightsByWorkflow = {
|
||||
count: 2,
|
||||
data: [
|
||||
{
|
||||
workflowId: 'workflow-1',
|
||||
workflowName: 'Test Workflow 1',
|
||||
total: 100,
|
||||
failed: 5,
|
||||
failureRate: 5,
|
||||
timeSaved: 45,
|
||||
averageRunTime: 12,
|
||||
projectId: 'project-1',
|
||||
projectName: 'Test Project 1',
|
||||
succeeded: 95,
|
||||
runTime: 1200,
|
||||
},
|
||||
{
|
||||
workflowId: 'workflow-2',
|
||||
workflowName: 'Test Workflow 2',
|
||||
total: 50,
|
||||
failed: 2,
|
||||
failureRate: 4,
|
||||
timeSaved: 20,
|
||||
averageRunTime: 8,
|
||||
projectId: 'project-2',
|
||||
projectName: 'Test Project 2',
|
||||
succeeded: 48,
|
||||
runTime: 400,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let insightsStore: MockedStore<typeof useInsightsStore>;
|
||||
|
||||
describe('InsightsDashboard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockRoute.params.insightType = INSIGHT_TYPES.TOTAL;
|
||||
|
||||
createTestingPinia({
|
||||
initialState: { settings: { settings: defaultSettings, moduleSettings } },
|
||||
});
|
||||
|
||||
insightsStore = mockedStore(useInsightsStore);
|
||||
|
||||
insightsStore.isSummaryEnabled = true;
|
||||
insightsStore.isDashboardEnabled = true;
|
||||
|
||||
// Mock async states
|
||||
insightsStore.summary = {
|
||||
state: mockSummaryData,
|
||||
isLoading: false,
|
||||
execute: vi.fn(),
|
||||
isReady: true,
|
||||
error: null,
|
||||
then: vi.fn(),
|
||||
};
|
||||
|
||||
insightsStore.charts = {
|
||||
state: mockChartsData,
|
||||
isLoading: false,
|
||||
execute: vi.fn(),
|
||||
isReady: true,
|
||||
error: null,
|
||||
then: vi.fn(),
|
||||
};
|
||||
|
||||
insightsStore.table = {
|
||||
state: mockTableData,
|
||||
isLoading: false,
|
||||
execute: vi.fn(),
|
||||
isReady: true,
|
||||
error: null,
|
||||
then: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should render without error', () => {
|
||||
mockedStore(useInsightsStore);
|
||||
expect(() => renderComponent()).not.toThrow();
|
||||
expect(document.title).toBe('Insights - n8n');
|
||||
describe('Component Rendering', () => {
|
||||
it('should render without error', () => {
|
||||
expect(() =>
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(document.title).toBe('Insights - n8n');
|
||||
expect(screen.getByRole('heading', { level: 2, name: 'Insights' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render summary when enabled', () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
expect(screen.getByTestId('insights-summary-tabs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render summary when disabled', () => {
|
||||
insightsStore.isSummaryEnabled = false;
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
expect(screen.queryByTestId('insights-summary-tabs')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render chart and table when dashboard enabled', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('insights-chart-total')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render chart when in time saved route even if dashboard disabled', async () => {
|
||||
insightsStore.isDashboardEnabled = false;
|
||||
mockRoute.params.insightType = INSIGHT_TYPES.TIME_SAVED;
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TIME_SAVED },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('insights-chart-time-saved')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render paywall when dashboard disabled and not time saved route', async () => {
|
||||
insightsStore.isDashboardEnabled = false;
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('insights-chart-total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('insights-table')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 4,
|
||||
name: 'Upgrade to access more detailed insights',
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the selected time range', async () => {
|
||||
mockedStore(useInsightsStore);
|
||||
describe('Date Range Selection', () => {
|
||||
it('should update the selected time range', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(screen.getByTestId('range-select')).toBeVisible();
|
||||
const select = within(screen.getByTestId('range-select')).getByRole('combobox');
|
||||
await userEvent.click(select);
|
||||
|
||||
expect(getByTestId('range-select')).toBeVisible();
|
||||
const select = within(getByTestId('range-select')).getByRole('combobox');
|
||||
await userEvent.click(select);
|
||||
const controllingId = select.getAttribute('aria-controls');
|
||||
const actions = document.querySelector(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
|
||||
const controllingId = select.getAttribute('aria-controls');
|
||||
const actions = document.querySelector(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
await userEvent.click(actions.querySelectorAll('li')[0]);
|
||||
expect((select as HTMLInputElement).value).toBe('Last 24 hours');
|
||||
|
||||
await userEvent.click(actions.querySelectorAll('li')[0]);
|
||||
expect((select as HTMLInputElement).value).toBe('Last 24 hours');
|
||||
expect(mockTelemetry.track).toHaveBeenCalledWith('User updated insights time range', {
|
||||
range: 1,
|
||||
});
|
||||
|
||||
expect(insightsStore.summary.execute).toHaveBeenCalledWith(0, { dateRange: 'day' });
|
||||
expect(insightsStore.charts.execute).toHaveBeenCalledWith(0, { dateRange: 'day' });
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'day',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show upgrade modal when unlicensed time range selected ', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('range-select')).toBeVisible();
|
||||
const select = within(screen.getByTestId('range-select')).getByRole('combobox');
|
||||
await userEvent.click(select);
|
||||
|
||||
const controllingId = select.getAttribute('aria-controls');
|
||||
const actions = document.querySelector(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
|
||||
// Select a range that requires an enterprise plan
|
||||
await userEvent.click(actions.querySelectorAll('li')[3]);
|
||||
|
||||
// Verify the select value is remained the original, default value, as unlicensed options should not change the selection
|
||||
expect((select as HTMLInputElement).value).toBe('Last 7 days');
|
||||
|
||||
expect(mockTelemetry.track).not.toHaveBeenCalled();
|
||||
expect(insightsStore.summary.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.charts.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/Viewing this time period requires an enterprise plan/),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Lifecycle', () => {
|
||||
it('should execute data fetching on mount', () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
expect(insightsStore.summary.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.charts.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
|
||||
it('should refetch data when insight type changes', async () => {
|
||||
const { rerender } = renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
await rerender({ insightType: INSIGHT_TYPES.FAILED });
|
||||
|
||||
expect(insightsStore.summary.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.charts.execute).toHaveBeenCalledWith(0, { dateRange: 'week' });
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'failed:desc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update sort order when insight type changes', async () => {
|
||||
const { rerender } = renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await rerender({ insightType: INSIGHT_TYPES.TIME_SAVED });
|
||||
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'timeSaved:desc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chart wrapper', () => {
|
||||
test.each([
|
||||
[INSIGHT_TYPES.TOTAL, 'insights-chart-total'],
|
||||
[INSIGHT_TYPES.FAILED, 'insights-chart-failed'],
|
||||
[INSIGHT_TYPES.FAILURE_RATE, 'insights-chart-failure-rate'],
|
||||
[INSIGHT_TYPES.TIME_SAVED, 'insights-chart-time-saved'],
|
||||
[INSIGHT_TYPES.AVERAGE_RUN_TIME, 'insights-chart-average-runtime'],
|
||||
])('should render %s chart component', async (type, testId) => {
|
||||
renderComponent({
|
||||
props: { insightType: type },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(testId)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table Functionality', () => {
|
||||
it('should handle table pagination', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await waitAllPromises();
|
||||
|
||||
// Simulate pagination event
|
||||
emitters.n8nDataTableServer.emit('update:options', {
|
||||
page: 1,
|
||||
itemsPerPage: 50,
|
||||
sortBy: [{ id: 'total', desc: true }],
|
||||
});
|
||||
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 50,
|
||||
take: 50,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle table sorting', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await waitAllPromises();
|
||||
|
||||
// Simulate sort event
|
||||
emitters.n8nDataTableServer.emit('update:options', {
|
||||
page: 0,
|
||||
itemsPerPage: 25,
|
||||
sortBy: [{ id: 'failed', desc: false }],
|
||||
});
|
||||
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: 'failed:asc',
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty sort array', async () => {
|
||||
renderComponent({
|
||||
props: { insightType: INSIGHT_TYPES.TOTAL },
|
||||
});
|
||||
|
||||
await waitAllPromises();
|
||||
|
||||
// Simulate event with no sortBy
|
||||
emitters.n8nDataTableServer.emit('update:options', {
|
||||
page: 0,
|
||||
itemsPerPage: 25,
|
||||
sortBy: [],
|
||||
});
|
||||
|
||||
expect(insightsStore.table.execute).toHaveBeenCalledWith(0, {
|
||||
skip: 0,
|
||||
take: 25,
|
||||
sortBy: undefined,
|
||||
dateRange: 'week',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type { InsightsDateRange, InsightsSummaryType } from '@n8n/api-types';
|
||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||
import { TELEMETRY_TIME_RANGE, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||
import { INSIGHT_TYPES, TELEMETRY_TIME_RANGE, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||
import InsightsDateRangeSelect from './InsightsDateRangeSelect.vue';
|
||||
import InsightsUpgradeModal from './InsightsUpgradeModal.vue';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
@@ -36,11 +37,14 @@ const props = defineProps<{
|
||||
insightType: InsightsSummaryType;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const insightsStore = useInsightsStore();
|
||||
|
||||
const isTimeSavedRoute = computed(() => route.params.insightType === INSIGHT_TYPES.TIME_SAVED);
|
||||
|
||||
const chartComponents = computed(() => ({
|
||||
total: InsightsChartTotal,
|
||||
failed: InsightsChartFailed,
|
||||
@@ -108,8 +112,8 @@ watch(
|
||||
void insightsStore.summary.execute(0, { dateRange: selectedDateRange.value });
|
||||
}
|
||||
|
||||
void insightsStore.charts.execute(0, { dateRange: selectedDateRange.value });
|
||||
if (insightsStore.isDashboardEnabled) {
|
||||
void insightsStore.charts.execute(0, { dateRange: selectedDateRange.value });
|
||||
fetchPaginatedTableData({ sortBy: sortTableBy.value, dateRange: selectedDateRange.value });
|
||||
}
|
||||
},
|
||||
@@ -149,11 +153,10 @@ onMounted(() => {
|
||||
:class="$style.insightsBanner"
|
||||
/>
|
||||
<div :class="$style.insightsContent">
|
||||
<InsightsPaywall
|
||||
v-if="!insightsStore.isDashboardEnabled"
|
||||
data-test-id="insights-dashboard-unlicensed"
|
||||
/>
|
||||
<div v-else :class="$style.insightsContentWrapper">
|
||||
<div
|
||||
v-if="insightsStore.isDashboardEnabled || isTimeSavedRoute"
|
||||
:class="$style.insightsContentWrapper"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
$style.dataLoader,
|
||||
@@ -179,10 +182,12 @@ onMounted(() => {
|
||||
v-model:sort-by="sortTableBy"
|
||||
:data="insightsStore.table.state"
|
||||
:loading="insightsStore.table.isLoading"
|
||||
:is-dashboard-enabled="insightsStore.isDashboardEnabled"
|
||||
@update:options="fetchPaginatedTableData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InsightsPaywall v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const goToUpgrade = async () => {
|
||||
<template>
|
||||
<div :class="$style.callout">
|
||||
<N8nIcon icon="lock" size="xlarge"></N8nIcon>
|
||||
<N8nText bold tag="h3" size="large">
|
||||
<N8nText bold tag="h4" size="large">
|
||||
{{ i18n.baseText('insights.dashboard.paywall.title') }}
|
||||
</N8nText>
|
||||
<N8nText>
|
||||
|
||||
@@ -63,7 +63,12 @@ const chartData = computed<ChartData<'line'>>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Line :data="chartData" :options="chartOptions" :plugins="[Filler]" />
|
||||
<Line
|
||||
data-test-id="insights-chart-average-runtime"
|
||||
:data="chartData"
|
||||
:options="chartOptions"
|
||||
:plugins="[Filler]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
|
||||
@@ -53,7 +53,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
<Bar data-test-id="insights-chart-failed" :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
|
||||
@@ -56,7 +56,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
<Bar data-test-id="insights-chart-failure-rate" :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
|
||||
@@ -72,7 +72,12 @@ const chartData = computed<ChartData<'line'>>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Line :data="chartData" :options="chartOptions" :plugins="[Filler]" />
|
||||
<Line
|
||||
data-test-id="insights-chart-time-saved"
|
||||
:data="chartData"
|
||||
:options="chartOptions"
|
||||
:plugins="[Filler]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
|
||||
@@ -53,7 +53,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
<Bar data-test-id="insights-chart-total" :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { screen, within } from '@testing-library/vue';
|
||||
import { screen, waitFor, within } from '@testing-library/vue';
|
||||
import { vi } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
@@ -10,7 +10,6 @@ import type { InsightsByWorkflow } from '@n8n/api-types';
|
||||
|
||||
const { emitters, addEmitter } = useEmitters<'n8nDataTableServer'>();
|
||||
|
||||
// Mock telemetry
|
||||
const mockTelemetry = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
@@ -18,7 +17,6 @@ vi.mock('@/composables/useTelemetry', () => ({
|
||||
useTelemetry: () => mockTelemetry,
|
||||
}));
|
||||
|
||||
// Mock N8nDataTableServer like in SettingsUsersTable.test.ts
|
||||
vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||
const original = await importOriginal<object>();
|
||||
return {
|
||||
@@ -37,6 +35,20 @@ vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||
},
|
||||
template: `
|
||||
<div data-test-id="insights-table">
|
||||
<div class="table-header">
|
||||
<div v-for="header in headers" :key="header.key">
|
||||
<button
|
||||
v-if="!header.disableSort"
|
||||
:data-test-id="'sort-' + header.key"
|
||||
@click="$emit('update:sortBy', [{ id: header.key, desc: false }])"
|
||||
>
|
||||
{{ header.title }}
|
||||
</button>
|
||||
<span v-else :data-test-id="'header-' + header.key">
|
||||
{{ header.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="item in items" :key="item.workflowId"
|
||||
:data-test-id="'workflow-row-' + item.workflowId">
|
||||
<div v-for="header in headers" :key="header.key">
|
||||
@@ -47,6 +59,7 @@ vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="cover" />
|
||||
</div>`,
|
||||
}),
|
||||
};
|
||||
@@ -93,6 +106,7 @@ describe('InsightsTableWorkflows', () => {
|
||||
props: {
|
||||
data: mockInsightsData,
|
||||
loading: false,
|
||||
isDashboardEnabled: true, // Default to true for basic tests
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -112,7 +126,7 @@ describe('InsightsTableWorkflows', () => {
|
||||
it('should display the correct heading', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Workflow insights' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Breakdown by workflow' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render workflow data in table rows', () => {
|
||||
@@ -148,16 +162,14 @@ describe('InsightsTableWorkflows', () => {
|
||||
],
|
||||
};
|
||||
|
||||
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||
pinia: createTestingPinia(),
|
||||
renderComponent({
|
||||
props: {
|
||||
data: largeNumberData,
|
||||
loading: false,
|
||||
isDashboardEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent();
|
||||
|
||||
const row = screen.getByTestId('workflow-row-workflow-1');
|
||||
expect(within(row).getByText('1,000,000')).toBeInTheDocument();
|
||||
expect(within(row).getByText('50,000')).toBeInTheDocument();
|
||||
@@ -322,4 +334,112 @@ describe('InsightsTableWorkflows', () => {
|
||||
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('paywall functionality', () => {
|
||||
it('should not display paywall when dashboard is enabled', () => {
|
||||
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||
pinia: createTestingPinia(),
|
||||
props: {
|
||||
data: mockInsightsData,
|
||||
loading: false,
|
||||
isDashboardEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'Upgrade to access more detailed insights' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display paywall when dashboard is not enabled', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
data: mockInsightsData,
|
||||
loading: false,
|
||||
isDashboardEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 4,
|
||||
name: 'Upgrade to access more detailed insights',
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use sample data when dashboard is not enabled', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
data: mockInsightsData,
|
||||
loading: false,
|
||||
isDashboardEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Should render sample workflows instead of actual data
|
||||
expect(screen.getByTestId('workflow-row-sample-workflow-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('workflow-row-sample-workflow-2')).toBeInTheDocument();
|
||||
// Should not render the original test data
|
||||
expect(screen.queryByTestId('workflow-row-workflow-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('workflow-row-workflow-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable sorting when dashboard is not enabled', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
data: mockInsightsData,
|
||||
loading: false,
|
||||
isDashboardEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
// When sorting is disabled, columns should not have clickable sort buttons
|
||||
expect(screen.queryByTestId('sort-workflowName')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-failed')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-failureRate')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-timeSaved')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-averageRunTime')).not.toBeInTheDocument();
|
||||
|
||||
// Headers should be present as non-clickable elements
|
||||
expect(screen.getByTestId('header-workflowName')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-total')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-failed')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-failureRate')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-timeSaved')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-averageRunTime')).toBeInTheDocument();
|
||||
// projectName is always disabled
|
||||
expect(screen.getByTestId('header-projectName')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should enable sorting when dashboard is enabled', () => {
|
||||
renderComponent();
|
||||
|
||||
// When sorting is enabled, most columns should have clickable sort buttons
|
||||
expect(screen.getByTestId('sort-workflowName')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sort-total')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sort-failed')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sort-failureRate')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sort-timeSaved')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sort-averageRunTime')).toBeInTheDocument();
|
||||
|
||||
// projectName is always disabled (non-clickable)
|
||||
expect(screen.getByTestId('header-projectName')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sort-projectName')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should trigger sort when clicking on sortable column header', async () => {
|
||||
const { emitted } = renderComponent();
|
||||
|
||||
const sortButton = screen.getByTestId('sort-workflowName');
|
||||
await userEvent.click(sortButton);
|
||||
|
||||
expect(emitted()['update:sortBy']).toEqual([[[{ id: 'workflowName', desc: false }]]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,12 +12,17 @@ import type { TableHeader } from '@n8n/design-system/components/N8nDataTableServ
|
||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { type RouteLocationRaw, type LocationQueryRaw } from 'vue-router';
|
||||
|
||||
const InsightsPaywall = defineAsyncComponent(
|
||||
async () => await import('@/features/insights/components/InsightsPaywall.vue'),
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByWorkflow;
|
||||
loading?: boolean;
|
||||
isDashboardEnabled?: boolean;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
@@ -25,13 +30,34 @@ const telemetry = useTelemetry();
|
||||
|
||||
type Item = InsightsByWorkflow['data'][number];
|
||||
|
||||
const rows = computed(() => props.data.data);
|
||||
const sampleWorkflows: InsightsByWorkflow['data'] = Array.from({ length: 10 }, (_, i) => ({
|
||||
workflowId: `sample-workflow-${i + 1}`,
|
||||
workflowName: `Sample Workflow ${i + 1}`,
|
||||
total: Math.floor(Math.random() * 2000) + 500,
|
||||
failed: Math.floor(Math.random() * 100) + 20,
|
||||
failureRate: Math.random() * 100,
|
||||
timeSaved: Math.floor(Math.random() * 300000) + 60000,
|
||||
averageRunTime: Math.floor(Math.random() * 60000) + 15000,
|
||||
projectName: `Sample Project ${i + 1}`,
|
||||
projectId: `sample-project-${i + 1}`,
|
||||
succeeded: Math.floor(Math.random() * 2000) + 500,
|
||||
runTime: Math.floor(Math.random() * 60000) + 15000,
|
||||
}));
|
||||
|
||||
const sampleData: InsightsByWorkflow = {
|
||||
data: sampleWorkflows,
|
||||
count: sampleWorkflows.length,
|
||||
};
|
||||
|
||||
const tableData = computed(() => (props.isDashboardEnabled ? props.data : sampleData));
|
||||
const rows = computed(() => tableData.value.data);
|
||||
|
||||
const headers = ref<Array<TableHeader<Item>>>([
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'workflowName',
|
||||
width: 400,
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.banner.title.total'),
|
||||
@@ -39,6 +65,7 @@ const headers = ref<Array<TableHeader<Item>>>([
|
||||
value(row) {
|
||||
return row.total.toLocaleString('en-US');
|
||||
},
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.banner.title.failed'),
|
||||
@@ -46,6 +73,7 @@ const headers = ref<Array<TableHeader<Item>>>([
|
||||
value(row) {
|
||||
return row.failed.toLocaleString('en-US');
|
||||
},
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.banner.title.failureRate'),
|
||||
@@ -56,6 +84,7 @@ const headers = ref<Array<TableHeader<Item>>>([
|
||||
INSIGHTS_UNIT_MAPPING.failureRate(row.failureRate)
|
||||
);
|
||||
},
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.banner.title.timeSaved'),
|
||||
@@ -66,6 +95,7 @@ const headers = ref<Array<TableHeader<Item>>>([
|
||||
INSIGHTS_UNIT_MAPPING.timeSaved(row.timeSaved)
|
||||
);
|
||||
},
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.banner.title.averageRunTime'),
|
||||
@@ -76,6 +106,7 @@ const headers = ref<Array<TableHeader<Item>>>([
|
||||
INSIGHTS_UNIT_MAPPING.averageRunTime(row.averageRunTime)
|
||||
);
|
||||
},
|
||||
disableSort: !props.isDashboardEnabled,
|
||||
},
|
||||
{
|
||||
title: i18n.baseText('insights.dashboard.table.projectName'),
|
||||
@@ -124,20 +155,26 @@ watch(sortBy, (newValue) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<N8nHeading bold tag="h3" size="medium" class="mb-s">Workflow insights</N8nHeading>
|
||||
<N8nHeading bold tag="h3" size="medium" class="mb-s">{{
|
||||
i18n.baseText('insights.dashboard.table.title')
|
||||
}}</N8nHeading>
|
||||
<N8nDataTableServer
|
||||
v-model:sort-by="sortBy"
|
||||
v-model:page="currentPage"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:items="rows"
|
||||
:headers="headers"
|
||||
:items-length="data.count"
|
||||
:items-length="tableData.count"
|
||||
@update:options="emit('update:options', $event)"
|
||||
>
|
||||
<template #[`item.workflowName`]="{ item }">
|
||||
<router-link :to="getWorkflowLink(item)" class="link" @click="trackWorkflowClick(item)">
|
||||
<router-link
|
||||
:to="getWorkflowLink(item)"
|
||||
:class="$style.link"
|
||||
@click="trackWorkflowClick(item)"
|
||||
>
|
||||
<N8nTooltip :content="item.workflowName" placement="top">
|
||||
<div class="ellipsis">
|
||||
<div :class="$style.ellipsis">
|
||||
{{ item.workflowName }}
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
@@ -147,7 +184,7 @@ watch(sortBy, (newValue) => {
|
||||
<router-link
|
||||
v-if="!item.timeSaved"
|
||||
:to="getWorkflowLink(item, { settings: 'true' })"
|
||||
class="link"
|
||||
:class="$style.link"
|
||||
>
|
||||
{{ i18n.baseText('insights.dashboard.table.estimate') }}
|
||||
</router-link>
|
||||
@@ -157,17 +194,22 @@ watch(sortBy, (newValue) => {
|
||||
</template>
|
||||
<template #[`item.projectName`]="{ item }">
|
||||
<N8nTooltip v-if="item.projectName" :content="item.projectName" placement="top">
|
||||
<div class="ellipsis">
|
||||
<div :class="$style.ellipsis">
|
||||
{{ item.projectName }}
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
<template v-else> - </template>
|
||||
</template>
|
||||
<template v-if="!isDashboardEnabled" #cover>
|
||||
<div :class="$style.blurryCover">
|
||||
<InsightsPaywall />
|
||||
</div>
|
||||
</template>
|
||||
</N8nDataTableServer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -188,4 +230,29 @@ watch(sortBy, (newValue) => {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.blurryCover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-foreground-xlight);
|
||||
opacity: 0.5;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,12 @@ export const fetchInsightsByTime = async (
|
||||
): Promise<InsightsByTime[]> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/by-time', filter);
|
||||
|
||||
export const fetchInsightsTimeSaved = async (
|
||||
context: IRestApiContext,
|
||||
filter?: { dateRange: InsightsDateRange['key'] },
|
||||
): Promise<InsightsByTime[]> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/by-time/time-saved', filter);
|
||||
|
||||
export const fetchInsightsByWorkflow = async (
|
||||
context: IRestApiContext,
|
||||
filter?: ListInsightsWorkflowQueryDto,
|
||||
|
||||
@@ -2,13 +2,21 @@ import type { InsightsSummaryType } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
export const INSIGHT_TYPES = {
|
||||
TOTAL: 'total',
|
||||
FAILED: 'failed',
|
||||
FAILURE_RATE: 'failureRate',
|
||||
TIME_SAVED: 'timeSaved',
|
||||
AVERAGE_RUN_TIME: 'averageRunTime',
|
||||
} as const;
|
||||
|
||||
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
||||
'total',
|
||||
'failed',
|
||||
'failureRate',
|
||||
'timeSaved',
|
||||
'averageRunTime',
|
||||
];
|
||||
INSIGHT_TYPES.TOTAL,
|
||||
INSIGHT_TYPES.FAILED,
|
||||
INSIGHT_TYPES.FAILURE_RATE,
|
||||
INSIGHT_TYPES.TIME_SAVED,
|
||||
INSIGHT_TYPES.AVERAGE_RUN_TIME,
|
||||
] as const;
|
||||
|
||||
export const INSIGHTS_UNIT_MAPPING: Record<InsightsSummaryType, (value: number) => string> = {
|
||||
total: () => '',
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import * as insightsApi from '@/features/insights/insights.api';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { mockedStore, type MockedStore } from '@/__tests__/utils';
|
||||
import type { IUser } from '@/Interface';
|
||||
import { reactive } from 'vue';
|
||||
import type { FrontendModuleSettings } from '@n8n/api-types';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => reactive({}),
|
||||
}));
|
||||
vi.mock('@/features/insights/insights.api');
|
||||
|
||||
const mockFilter = { dateRange: 'week' as const };
|
||||
const mockData = [
|
||||
{
|
||||
date: '2023-01-01',
|
||||
values: {
|
||||
total: 100,
|
||||
failed: 10,
|
||||
failureRate: 10,
|
||||
timeSaved: 50,
|
||||
averageRunTime: 5,
|
||||
succeeded: 90,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('useInsightsStore', () => {
|
||||
let insightsStore: ReturnType<typeof useInsightsStore>;
|
||||
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||
let rootStore: MockedStore<typeof useRootStore>;
|
||||
let usersStore: MockedStore<typeof useUsersStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
|
||||
settingsStore = mockedStore(useSettingsStore);
|
||||
rootStore = mockedStore(useRootStore);
|
||||
usersStore = mockedStore(useUsersStore);
|
||||
|
||||
rootStore.restApiContext = {
|
||||
baseUrl: 'http://localhost',
|
||||
pushRef: 'pushRef',
|
||||
};
|
||||
|
||||
usersStore.currentUser = { globalScopes: ['insights:list'] } as IUser;
|
||||
|
||||
insightsStore = useInsightsStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('charts data fetcher', () => {
|
||||
it('should use fetchInsightsTimeSaved when dashboard is disabled', async () => {
|
||||
settingsStore.moduleSettings = { insights: { dashboard: false } } as FrontendModuleSettings;
|
||||
|
||||
vi.mocked(insightsApi.fetchInsightsTimeSaved).mockResolvedValue(mockData);
|
||||
|
||||
await insightsStore.charts.execute(0, mockFilter);
|
||||
|
||||
expect(insightsApi.fetchInsightsTimeSaved).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
mockFilter,
|
||||
);
|
||||
expect(insightsApi.fetchInsightsByTime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use fetchInsightsByTime when dashboard is enabled', async () => {
|
||||
settingsStore.moduleSettings = { insights: { dashboard: true } } as FrontendModuleSettings;
|
||||
|
||||
vi.mocked(insightsApi.fetchInsightsByTime).mockResolvedValue(mockData);
|
||||
|
||||
await insightsStore.charts.execute(0, mockFilter);
|
||||
|
||||
expect(insightsApi.fetchInsightsByTime).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
mockFilter,
|
||||
);
|
||||
expect(insightsApi.fetchInsightsTimeSaved).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use fetchInsightsTimeSaved when dashboard setting is undefined', async () => {
|
||||
settingsStore.moduleSettings = { insights: {} } as FrontendModuleSettings;
|
||||
|
||||
vi.mocked(insightsApi.fetchInsightsTimeSaved).mockResolvedValue(mockData);
|
||||
|
||||
await insightsStore.charts.execute(0, mockFilter);
|
||||
|
||||
expect(insightsApi.fetchInsightsTimeSaved).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
mockFilter,
|
||||
);
|
||||
expect(insightsApi.fetchInsightsByTime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work without filter parameter', async () => {
|
||||
settingsStore.moduleSettings = { insights: { dashboard: true } } as FrontendModuleSettings;
|
||||
|
||||
vi.mocked(insightsApi.fetchInsightsByTime).mockResolvedValue(mockData);
|
||||
|
||||
await insightsStore.charts.execute();
|
||||
|
||||
expect(insightsApi.fetchInsightsByTime).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,9 +22,7 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
settingsStore.settings.activeModules.includes('insights'),
|
||||
);
|
||||
|
||||
const isDashboardEnabled = computed(
|
||||
() => settingsStore.moduleSettings.insights?.dashboard ?? false,
|
||||
);
|
||||
const isDashboardEnabled = computed(() => !!settingsStore.moduleSettings.insights?.dashboard);
|
||||
|
||||
const isSummaryEnabled = computed(
|
||||
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
||||
@@ -52,7 +50,10 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
|
||||
const charts = useAsyncState(
|
||||
async (filter?: { dateRange: InsightsDateRange['key'] }) => {
|
||||
return await insightsApi.fetchInsightsByTime(rootStore.restApiContext, filter);
|
||||
const dataFetcher = isDashboardEnabled.value
|
||||
? insightsApi.fetchInsightsByTime
|
||||
: insightsApi.fetchInsightsTimeSaved;
|
||||
return await dataFetcher(rootStore.restApiContext, filter);
|
||||
},
|
||||
[],
|
||||
{ immediate: false, resetOnExecute: false },
|
||||
|
||||
Reference in New Issue
Block a user