mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 11:35:03 +02:00
feat(core): Show alert when selected time range for insights lacks data (#33180)
This commit is contained in:
parent
6c85fdcd81
commit
f4e7016c0c
|
|
@ -272,6 +272,7 @@ export type FrontendModuleSettings = {
|
|||
summary: boolean;
|
||||
dashboard: boolean;
|
||||
dateRanges: InsightsDateRange[];
|
||||
earliestDataDate: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { MockProxy } from 'jest-mock-extended';
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
|
||||
import { InsightsModule } from '../insights.module';
|
||||
import { InsightsService } from '../insights.service';
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ describe('InsightsModule', () => {
|
|||
Container.set(InstanceSettings, mockInstanceSettings);
|
||||
Container.set(Logger, mockLogger());
|
||||
Container.set(LicenseState, mock<LicenseState>());
|
||||
Container.set(InsightsByPeriodRepository, mock<InsightsByPeriodRepository>());
|
||||
Container.set(
|
||||
InsightsService,
|
||||
new InsightsService(
|
||||
|
|
|
|||
|
|
@ -1,33 +1,37 @@
|
|||
import type { LicenseState } from '@n8n/backend-common';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
|
||||
import { InsightsSettings } from '../insights.settings';
|
||||
|
||||
describe('InsightsSettings', () => {
|
||||
let licenseMock: jest.Mocked<LicenseState>;
|
||||
let repositoryMock: jest.Mocked<InsightsByPeriodRepository>;
|
||||
let insightsSettings: InsightsSettings;
|
||||
|
||||
beforeAll(() => {
|
||||
licenseMock = mock<LicenseState>();
|
||||
insightsSettings = new InsightsSettings(licenseMock);
|
||||
repositoryMock = mock<InsightsByPeriodRepository>();
|
||||
repositoryMock.getEarliestDataDate.mockResolvedValue(null);
|
||||
insightsSettings = new InsightsSettings(licenseMock, repositoryMock);
|
||||
});
|
||||
|
||||
test('returns correct summary and dashboard licenses', () => {
|
||||
test('returns correct summary and dashboard licenses', async () => {
|
||||
licenseMock.isInsightsSummaryLicensed.mockReturnValue(true);
|
||||
licenseMock.isInsightsDashboardLicensed.mockReturnValue(true);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.summary).toBe(true);
|
||||
expect(result.dashboard).toBe(true);
|
||||
});
|
||||
|
||||
describe('dateRanges', () => {
|
||||
test('returns correct ranges when hourly data is enabled and max history is unlimited', () => {
|
||||
test('returns correct ranges when hourly data is enabled and max history is unlimited', async () => {
|
||||
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
||||
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.dateRanges).toEqual([
|
||||
{ key: 'day', licensed: true, granularity: 'hour' },
|
||||
|
|
@ -40,11 +44,11 @@ describe('InsightsSettings', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('returns correct ranges when hourly data is enabled and max history is 365 days', () => {
|
||||
test('returns correct ranges when hourly data is enabled and max history is 365 days', async () => {
|
||||
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.dateRanges).toEqual([
|
||||
{ key: 'day', licensed: true, granularity: 'hour' },
|
||||
|
|
@ -57,11 +61,11 @@ describe('InsightsSettings', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('returns correct ranges when hourly data is disabled and max history is 30 days', () => {
|
||||
test('returns correct ranges when hourly data is disabled and max history is 30 days', async () => {
|
||||
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
||||
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.dateRanges).toEqual([
|
||||
{ key: 'day', licensed: false, granularity: 'hour' },
|
||||
|
|
@ -74,11 +78,11 @@ describe('InsightsSettings', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('returns correct ranges when max history is less than 7 days', () => {
|
||||
test('returns correct ranges when max history is less than 7 days', async () => {
|
||||
licenseMock.getInsightsMaxHistory.mockReturnValue(5);
|
||||
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.dateRanges).toEqual([
|
||||
{ key: 'day', licensed: false, granularity: 'hour' },
|
||||
|
|
@ -91,11 +95,11 @@ describe('InsightsSettings', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('returns correct ranges when max history is 90 days and hourly data is enabled', () => {
|
||||
test('returns correct ranges when max history is 90 days and hourly data is enabled', async () => {
|
||||
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
||||
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||
|
||||
const result = insightsSettings.settings();
|
||||
const result = await insightsSettings.settings();
|
||||
|
||||
expect(result.dateRanges).toEqual([
|
||||
{ key: 'day', licensed: true, granularity: 'hour' },
|
||||
|
|
|
|||
|
|
@ -436,4 +436,11 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
|||
|
||||
return { affected: result.affected };
|
||||
}
|
||||
|
||||
async getEarliestDataDate(): Promise<Date | null> {
|
||||
const result = await this.createQueryBuilder('ibp')
|
||||
.select('MIN(ibp.periodStart)', 'minDate')
|
||||
.getRawOne<{ minDate: Date | string | null }>();
|
||||
return result?.minDate ? new Date(result.minDate) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class InsightsModule implements ModuleInterface {
|
|||
async settings() {
|
||||
const { InsightsSettings } = await import('./insights.settings');
|
||||
|
||||
return Container.get(InsightsSettings).settings();
|
||||
return await Container.get(InsightsSettings).settings();
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ import { LicenseState } from '@n8n/backend-common';
|
|||
import { Service } from '@n8n/di';
|
||||
|
||||
import { INSIGHTS_DATE_RANGE_KEYS, keyRangeToDays } from './insights.constants';
|
||||
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
|
||||
|
||||
@Service()
|
||||
export class InsightsSettings {
|
||||
constructor(private readonly licenseState: LicenseState) {}
|
||||
constructor(
|
||||
private readonly licenseState: LicenseState,
|
||||
private readonly insightsByPeriodRepository: InsightsByPeriodRepository,
|
||||
) {}
|
||||
|
||||
settings() {
|
||||
async settings() {
|
||||
const earliest = await this.insightsByPeriodRepository.getEarliestDataDate();
|
||||
return {
|
||||
summary: this.licenseState.isInsightsSummaryLicensed(),
|
||||
dashboard: this.licenseState.isInsightsDashboardLicensed(),
|
||||
dateRanges: this.getAvailableDateRanges(),
|
||||
earliestDataDate: earliest?.toISOString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5740,6 +5740,10 @@
|
|||
"insights.dashboard.table.estimate": "Estimate",
|
||||
"insights.dashboard.title": "Insights",
|
||||
"insights.dashboard.search.placeholder": "All projects",
|
||||
"insights.dashboard.dataRangeAlert.title": "Your selected range goes back further than your data",
|
||||
"insights.dashboard.dataRangeAlert.description": "Insights started collecting data on {date}. The 1 earlier day in your range does not have any data available. | Insights started collecting data on {date}. The {days} earlier days in your range do not have any data available.",
|
||||
"insights.dashboard.dataRangeAlert.descriptionNoData": "Insights started collecting data on {date}. Your selected range does not have any data available.",
|
||||
"insights.dashboard.dataRangeAlert.dismiss": "Dismiss",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const moduleSettings: FrontendModuleSettings = {
|
|||
insights: {
|
||||
summary: true,
|
||||
dashboard: true,
|
||||
earliestDataDate: null,
|
||||
dateRanges: [
|
||||
{
|
||||
key: 'day',
|
||||
|
|
@ -264,6 +265,46 @@ const selectProject = async (projectName: string | null) => {
|
|||
await userEvent.click(teamProject as Element);
|
||||
};
|
||||
|
||||
const setupStores = () => {
|
||||
insightsStore = mockedStore(useInsightsStore);
|
||||
projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
insightsStore.isSummaryEnabled = true;
|
||||
insightsStore.isDashboardEnabled = true;
|
||||
|
||||
projectsStore.availableProjects = projects;
|
||||
projectsStore.getAvailableProjects = vi.fn().mockResolvedValue(projects);
|
||||
projectsStore.searchProjects.mockResolvedValue({ count: projects.length, data: projects });
|
||||
projectsStore.globalProjectPermissions = { list: true };
|
||||
|
||||
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(),
|
||||
};
|
||||
};
|
||||
|
||||
describe('InsightsDashboard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -275,45 +316,7 @@ describe('InsightsDashboard', () => {
|
|||
initialState: { settings: { settings: defaultSettings, moduleSettings } },
|
||||
});
|
||||
|
||||
insightsStore = mockedStore(useInsightsStore);
|
||||
projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
insightsStore.isSummaryEnabled = true;
|
||||
insightsStore.isDashboardEnabled = true;
|
||||
|
||||
// Mock projects store
|
||||
projectsStore.availableProjects = projects;
|
||||
projectsStore.getAvailableProjects = vi.fn().mockResolvedValue(projects);
|
||||
projectsStore.searchProjects.mockResolvedValue({ count: projects.length, data: projects });
|
||||
projectsStore.globalProjectPermissions = { list: 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(),
|
||||
};
|
||||
setupStores();
|
||||
});
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
|
|
@ -719,4 +722,33 @@ describe('InsightsDashboard', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default date range initialization', () => {
|
||||
it('should default to 7 days ago when earliestDataDate is null', () => {
|
||||
const { getByRole } = renderComponent({ props: { insightType: INSIGHT_TYPES.TOTAL } });
|
||||
// System date is 2000-12-19, so 7 days ago is 2000-12-12
|
||||
expect(getByRole('button', { name: '12 Dec - 19 Dec, 2000' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should clamp default range start to the earliest date with data with a maximum of 7', () => {
|
||||
// earliestDataDate is Dec 15, which is within the last 7 days (Dec 12–19)
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
settings: {
|
||||
settings: defaultSettings,
|
||||
moduleSettings: {
|
||||
...moduleSettings,
|
||||
insights: {
|
||||
...moduleSettings.insights!,
|
||||
earliestDataDate: '2000-12-15T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setupStores();
|
||||
const { getByRole } = renderComponent({ props: { insightType: INSIGHT_TYPES.TOTAL } });
|
||||
expect(getByRole('button', { name: '15 Dec - 19 Dec, 2000' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useAvailableProjectSearch } from '@/features/collaboration/projects/pro
|
|||
import InsightsSummary from '@/features/execution/insights/components/InsightsSummary.vue';
|
||||
import { useInsightsStore } from '@/features/execution/insights/insights.store';
|
||||
import type { DateValue } from '@internationalized/date';
|
||||
import { getLocalTimeZone, today } from '@internationalized/date';
|
||||
import { getLocalTimeZone, parseDate, today } from '@internationalized/date';
|
||||
import type { InsightsSummaryType } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import {
|
||||
|
|
@ -24,6 +24,7 @@ import { useRoute } from 'vue-router';
|
|||
import { INSIGHT_TYPES } from '../insights.constants';
|
||||
import { getAdjustedDateRange, getTimeRangeLabels, timeRangeMappings } from '../insights.utils';
|
||||
import InsightsDataRangePicker from './InsightsDataRangePicker.vue';
|
||||
import InsightsDateRangeAlert from './InsightsDateRangeAlert.vue';
|
||||
|
||||
import { N8nHeading, N8nSpinner } from '@n8n/design-system';
|
||||
const InsightsPaywall = defineAsyncComponent(
|
||||
|
|
@ -114,11 +115,21 @@ const minimumValue = shallowRef(
|
|||
maxDate.copy().subtract({ days: timeRangeMappings[maxLicensedDate] }),
|
||||
);
|
||||
|
||||
function getDefaultRangeStart(): DateValue {
|
||||
const sevenDaysAgo = maxDate.copy().subtract({ days: 7 });
|
||||
if (insightsStore.earliestDataDate) {
|
||||
const earliestDate = parseDate(insightsStore.earliestDataDate.substring(0, 10));
|
||||
// If data only starts within the last 7 days, begin from first data point
|
||||
return earliestDate.compare(sevenDaysAgo) > 0 ? earliestDate : sevenDaysAgo;
|
||||
}
|
||||
return sevenDaysAgo;
|
||||
}
|
||||
|
||||
const range = shallowRef<{
|
||||
start: DateValue;
|
||||
end: DateValue;
|
||||
}>({
|
||||
start: maxDate.copy().subtract({ days: 7 }),
|
||||
start: getDefaultRangeStart(),
|
||||
end: maxDate.copy(),
|
||||
});
|
||||
|
||||
|
|
@ -236,6 +247,14 @@ onBeforeMount(async () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<InsightsDateRangeAlert
|
||||
:key="range.start.toString()"
|
||||
class="mt-s"
|
||||
:earliest-data-date="insightsStore.earliestDataDate"
|
||||
:range-start="range.start"
|
||||
:range-end="range.end"
|
||||
/>
|
||||
|
||||
<InsightsSummary
|
||||
v-if="insightsStore.isSummaryEnabled"
|
||||
:summary="insightsStore.summary.state"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { CalendarDate } from '@internationalized/date';
|
||||
import InsightsDateRangeAlert from '@/features/execution/insights/components/InsightsDateRangeAlert.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { defaultSettings } from '@/__tests__/defaults';
|
||||
|
||||
const renderComponent = createComponentRenderer(InsightsDateRangeAlert, {
|
||||
global: {
|
||||
stubs: { N8nIcon: true },
|
||||
},
|
||||
});
|
||||
|
||||
describe('InsightsDateRangeAlert', () => {
|
||||
beforeEach(() => {
|
||||
createTestingPinia({
|
||||
initialState: { settings: { settings: defaultSettings } },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render when earliestDataDate is null', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: null,
|
||||
rangeStart: new CalendarDate(2026, 6, 1),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('insights-date-range-alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render when rangeStart is on the earliest data date', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 11),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('insights-date-range-alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render when rangeStart is after the earliest data date', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 15),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('insights-date-range-alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the alert when rangeStart is before the earliest data date', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 9),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('insights-date-range-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the correct title', () => {
|
||||
const { getByText } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 9),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText('Your selected range goes back further than your data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the singular form when exactly 1 day has no data', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 10),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('insights-date-range-alert').textContent).toContain('1 earlier day');
|
||||
});
|
||||
|
||||
it('uses the plural form when multiple days have no data', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 9),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('insights-date-range-alert').textContent).toContain('2 earlier days');
|
||||
});
|
||||
|
||||
it('shows no-data description when the entire range is before the earliest data date', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-25T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 1),
|
||||
rangeEnd: new CalendarDate(2026, 6, 10),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('insights-date-range-alert').textContent).toContain(
|
||||
'Your selected range does not have any data available.',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides the alert and emits dismiss when the dismiss button is clicked', async () => {
|
||||
const { getByTestId, queryByTestId, emitted } = renderComponent({
|
||||
props: {
|
||||
earliestDataDate: '2026-06-11T00:00:00.000Z',
|
||||
rangeStart: new CalendarDate(2026, 6, 9),
|
||||
rangeEnd: new CalendarDate(2026, 6, 30),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('insights-date-range-alert')).toBeInTheDocument();
|
||||
|
||||
await getByTestId('insights-date-range-alert-dismiss').click();
|
||||
|
||||
expect(queryByTestId('insights-date-range-alert')).not.toBeInTheDocument();
|
||||
expect(emitted('dismiss')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
import type { DateValue } from '@internationalized/date';
|
||||
import { getLocalTimeZone, parseDate } from '@internationalized/date';
|
||||
import { N8nAlert, N8nButton, N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
earliestDataDate: string | null;
|
||||
rangeStart: DateValue;
|
||||
rangeEnd: DateValue;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ dismiss: [] }>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const isDismissed = ref(false);
|
||||
|
||||
const alertInfo = computed(() => {
|
||||
if (!props.earliestDataDate) return null;
|
||||
|
||||
const earliestCalendarDate = parseDate(props.earliestDataDate.substring(0, 10));
|
||||
if (props.rangeStart.compare(earliestCalendarDate) >= 0) return null;
|
||||
|
||||
if (props.rangeEnd.compare(earliestCalendarDate) < 0) {
|
||||
return { earliestCalendarDate, daysWithoutData: 0, noData: true as const };
|
||||
}
|
||||
|
||||
const daysWithoutData = earliestCalendarDate.compare(props.rangeStart);
|
||||
return { earliestCalendarDate, daysWithoutData, noData: false as const };
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!alertInfo.value) return '';
|
||||
const d = alertInfo.value.earliestCalendarDate.toDate(getLocalTimeZone());
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(d);
|
||||
});
|
||||
|
||||
function dismiss() {
|
||||
isDismissed.value = true;
|
||||
emit('dismiss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nAlert
|
||||
v-if="alertInfo && !isDismissed"
|
||||
type="info"
|
||||
:show-icon="false"
|
||||
data-test-id="insights-date-range-alert"
|
||||
>
|
||||
<template #icon>
|
||||
<N8nText color="text-dark">
|
||||
<N8nIcon icon="info" />
|
||||
</N8nText>
|
||||
</template>
|
||||
<template #title>
|
||||
<N8nText color="text-dark" bold>
|
||||
{{ i18n.baseText('insights.dashboard.dataRangeAlert.title') }}
|
||||
</N8nText>
|
||||
</template>
|
||||
<N8nText color="text-dark">
|
||||
{{
|
||||
alertInfo.noData
|
||||
? i18n.baseText('insights.dashboard.dataRangeAlert.descriptionNoData', {
|
||||
interpolate: { date: formattedDate },
|
||||
})
|
||||
: i18n.baseText('insights.dashboard.dataRangeAlert.description', {
|
||||
interpolate: { date: formattedDate, days: alertInfo.daysWithoutData },
|
||||
adjustToNumber: alertInfo.daysWithoutData,
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
<template #aside>
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
size="small"
|
||||
variant="subtle"
|
||||
:label="i18n.baseText('insights.dashboard.dataRangeAlert.dismiss')"
|
||||
data-test-id="insights-date-range-alert-dismiss"
|
||||
@click="dismiss"
|
||||
/>
|
||||
</template>
|
||||
</N8nAlert>
|
||||
</template>
|
||||
|
|
@ -68,6 +68,10 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||
|
||||
const dateRanges = computed(() => settingsStore.moduleSettings.insights?.dateRanges ?? []);
|
||||
|
||||
const earliestDataDate = computed(
|
||||
() => settingsStore.moduleSettings.insights?.earliestDataDate ?? null,
|
||||
);
|
||||
|
||||
return {
|
||||
globalInsightsPermissions,
|
||||
isInsightsEnabled,
|
||||
|
|
@ -78,5 +82,6 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||
charts,
|
||||
table,
|
||||
dateRanges,
|
||||
earliestDataDate,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user