diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 809dc68daa..c08fdce17a 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -1,5 +1,7 @@ import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow'; +import { type InsightsDateRange } from './schemas/insights.schema'; + export interface IVersionNotificationSettings { enabled: boolean; endpoint: string; @@ -193,5 +195,6 @@ export interface FrontendSettings { enabled: boolean; summary: boolean; dashboard: boolean; + dateRanges: InsightsDateRange[]; }; } diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 7335cd4cc5..0387c0ca68 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -251,6 +251,7 @@ export class FrontendService { enabled: this.modulesConfig.modules.includes('insights'), summary: true, dashboard: false, + dateRanges: [], }, logsView: { enabled: false, diff --git a/packages/frontend/editor-ui/src/__tests__/defaults.ts b/packages/frontend/editor-ui/src/__tests__/defaults.ts index e45167710a..47fc2a6d7b 100644 --- a/packages/frontend/editor-ui/src/__tests__/defaults.ts +++ b/packages/frontend/editor-ui/src/__tests__/defaults.ts @@ -149,6 +149,15 @@ export const defaultSettings: FrontendSettings = { enabled: false, summary: true, dashboard: false, + dateRanges: [ + { key: 'day', licensed: true, granularity: 'hour' }, + { key: 'week', licensed: true, granularity: 'day' }, + { key: '2weeks', licensed: true, granularity: 'day' }, + { key: 'month', licensed: false, granularity: 'day' }, + { key: 'quarter', licensed: false, granularity: 'week' }, + { key: '6months', licensed: false, granularity: 'week' }, + { key: 'year', licensed: false, granularity: 'week' }, + ], }, logsView: { enabled: false, diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.test.ts b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.test.ts new file mode 100644 index 0000000000..855643d9eb --- /dev/null +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.test.ts @@ -0,0 +1,44 @@ +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 userEvent from '@testing-library/user-event'; + +const renderComponent = createComponentRenderer(InsightsDashboard, { + props: { + insightType: 'total', + }, +}); + +describe('InsightsDashboard', () => { + beforeEach(() => { + createTestingPinia({ initialState: { settings: { settings: defaultSettings } } }); + }); + + it('should render without error', () => { + mockedStore(useInsightsStore); + expect(() => renderComponent()).not.toThrow(); + }); + + it('should update the selected time range', async () => { + mockedStore(useInsightsStore); + + const { getByTestId } = renderComponent(); + + 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'); + } + + await userEvent.click(actions.querySelectorAll('li')[0]); + expect((select as HTMLInputElement).value).toBe('Last 24 hours'); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue index f5b5b0d8b9..c457fa274f 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue @@ -1,9 +1,13 @@ + + + + + {{ item.label }} + + + + + + + diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.test.ts b/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.test.ts index 507423eab3..c0ce4f03e9 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.test.ts +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.test.ts @@ -27,6 +27,7 @@ describe('InsightsSummary', () => { renderComponent({ props: { summary: [], + timeRange: 'week', }, }), ).not.toThrow(); @@ -95,6 +96,7 @@ describe('InsightsSummary', () => { const { html } = renderComponent({ props: { summary, + timeRange: 'week', }, }); diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.vue b/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.vue index ef222edee3..8c34429afc 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.vue +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsSummary.vue @@ -5,15 +5,17 @@ import { VIEWS } from '@/constants'; import { INSIGHT_IMPACT_TYPES, INSIGHTS_UNIT_IMPACT_MAPPING, + TIME_RANGE_LABELS, } from '@/features/insights/insights.constants'; import type { InsightsSummaryDisplay } from '@/features/insights/insights.types'; -import type { InsightsSummary } from '@n8n/api-types'; +import type { InsightsDateRange, InsightsSummary } from '@n8n/api-types'; import { smartDecimal } from '@n8n/utils/number/smartDecimal'; -import { computed, ref, useCssModule } from 'vue'; +import { computed, useCssModule } from 'vue'; import { useRoute } from 'vue-router'; const props = defineProps<{ summary: InsightsSummaryDisplay; + timeRange: InsightsDateRange['key']; loading?: boolean; }>(); @@ -22,8 +24,6 @@ const route = useRoute(); const $style = useCssModule(); const telemetry = useTelemetry(); -const lastNDays = ref(7); - const summaryTitles = computed>(() => ({ total: i18n.baseText('insights.banner.title.total'), failed: i18n.baseText('insights.banner.title.failed'), @@ -84,9 +84,9 @@ const trackTabClick = (insightType: keyof InsightsSummary) => { {{ summaryTitles[id] }} - {{ - i18n.baseText('insights.lastNDays', { interpolate: { count: lastNDays } }) - }} + + {{ TIME_RANGE_LABELS[timeRange] }} + diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsUpgradeModal.vue b/packages/frontend/editor-ui/src/features/insights/components/InsightsUpgradeModal.vue new file mode 100644 index 0000000000..52273797f4 --- /dev/null +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsUpgradeModal.vue @@ -0,0 +1,70 @@ + + + + + + + {{ i18n.baseText('insights.upgradeModal.content') }} + + + + + + + {{ perk }} + + + + + + + {{ i18n.baseText('insights.upgradeModal.button.dismiss') }} + + + {{ i18n.baseText('insights.upgradeModal.button.upgrade') }} + + + + + + + diff --git a/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartAverageRuntime.vue b/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartAverageRuntime.vue index 41eb358537..181bf5bb39 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartAverageRuntime.vue +++ b/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartAverageRuntime.vue @@ -4,20 +4,18 @@ import { generateLinearGradient, generateLineChartOptions, } from '@/features/insights/chartjs.utils'; -import { DATE_FORMAT_MASK, INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants'; +import { + GRANULARITY_DATE_FORMAT_MASK, + INSIGHTS_UNIT_MAPPING, +} from '@/features/insights/insights.constants'; import { transformInsightsAverageRunTime } from '@/features/insights/insights.utils'; -import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types'; import { smartDecimal } from '@n8n/utils/number/smartDecimal'; import { type ChartData, Filler, type ScriptableContext } from 'chart.js'; -import dateformat from 'dateformat'; import { computed } from 'vue'; import { Line } from 'vue-chartjs'; +import type { ChartProps } from './insightChartProps'; -const props = defineProps<{ - data: InsightsByTime[]; - type: InsightsSummaryType; -}>(); - +const props = defineProps(); const i18n = useI18n(); const chartOptions = computed(() => @@ -40,7 +38,7 @@ const chartData = computed>(() => { const data: number[] = []; for (const entry of props.data) { - labels.push(dateformat(entry.date, DATE_FORMAT_MASK)); + labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date)); const value = transformInsightsAverageRunTime(entry.values.averageRunTime); diff --git a/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartFailed.vue b/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartFailed.vue index 244dee1242..b8dddd69d6 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartFailed.vue +++ b/packages/frontend/editor-ui/src/features/insights/components/charts/InsightsChartFailed.vue @@ -1,19 +1,16 @@