From ef9557f60ed2be762d17928a7b5e57cb8dcdead7 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 30 Dec 2025 16:08:46 +0100 Subject: [PATCH] Add time range warning in query editor for large time ranges (#2150) ## Summary Adds a non-intrusive warning banner in the query editor that alerts users when the selected time range exceeds 1 year (365 days). This helps users understand that their query may return a large amount of data and could take longer to execute, without blocking or interrupting their workflow. Part of https://github.com/grafana/oss-big-tent-squad/issues/127 ## Changes - Added `TIME_RANGE_WARNING_THRESHOLD_DAYS` constant (365 days) in `src/datasource/constants.ts` - Created new `TimeRangeWarning` component in `src/datasource/components/TimeRangeWarning.tsx` - Integrated the warning component into the main `QueryEditor` component ## How it works - When the dashboard time range is >= 365 days, a warning banner appears at the top of the query editor - The warning displays the formatted duration (e.g., "1 year and 30 days") - The warning is purely informational - queries still execute normally - Uses Grafana theme colors for consistent styling in both light and dark modes ## Screenshot The warning appears as a subtle banner with a warning icon: grafik ## Why Queries spanning years of data can return millions of data points and potentially overload the Zabbix server. This proactive warning helps users make informed decisions about their query scope without adding friction to the normal query flow. --- src/datasource/components/QueryEditor.tsx | 4 +- .../components/TimeRangeWarning.tsx | 28 ++++ .../components/timeRangeDuration.test.ts | 135 ++++++++++++++++++ .../components/timeRangeDuration.ts | 36 +++++ src/datasource/constants.ts | 2 + 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/datasource/components/TimeRangeWarning.tsx create mode 100644 src/datasource/components/timeRangeDuration.test.ts create mode 100644 src/datasource/components/timeRangeDuration.ts diff --git a/src/datasource/components/QueryEditor.tsx b/src/datasource/components/QueryEditor.tsx index 646ec71..720e18d 100644 --- a/src/datasource/components/QueryEditor.tsx +++ b/src/datasource/components/QueryEditor.tsx @@ -17,6 +17,7 @@ import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor'; import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor'; import { QueryEditorRow } from './QueryEditor/QueryEditorRow'; import { ItemCountWarning } from './ItemCountWarning'; +import { TimeRangeWarning } from './TimeRangeWarning'; const zabbixQueryTypeOptions: Array> = [ { @@ -113,7 +114,7 @@ function getProblemsQueryDefaults(): Partial { export interface ZabbixQueryEditorProps extends QueryEditorProps {} -export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => { +export const QueryEditor = ({ query, datasource, onChange, onRunQuery, range }: ZabbixQueryEditorProps) => { const [itemCount, setItemCount] = useState(0); const queryDefaults = getDefaultQuery(); query = { ...queryDefaults, ...query }; @@ -206,6 +207,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ return ( {queryType === c.MODE_METRICS && } + diff --git a/src/datasource/components/TimeRangeWarning.tsx b/src/datasource/components/TimeRangeWarning.tsx new file mode 100644 index 0000000..7c9b64f --- /dev/null +++ b/src/datasource/components/TimeRangeWarning.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Alert } from '@grafana/ui'; +import { formatDuration, getTimeRangeDurationDays, isTimeRangeLarge } from './timeRangeDuration'; +import { TimeRange } from '@grafana/data'; + +interface TimeRangeWarningProps { + timeRange?: TimeRange; +} + +/** + * A warning banner that displays when the selected time range is very large. + * This is a non-intrusive warning that doesn't block the query flow. + */ +export const TimeRangeWarning: React.FC = ({ timeRange }) => { + if (!timeRange || !isTimeRangeLarge(timeRange)) { + return null; + } + + const durationDays = getTimeRangeDurationDays(timeRange); + const formattedDuration = formatDuration(durationDays); + + return ( + + Selected time range is {formattedDuration}. This query may return a large amount of data and could take longer to + execute. + + ); +}; diff --git a/src/datasource/components/timeRangeDuration.test.ts b/src/datasource/components/timeRangeDuration.test.ts new file mode 100644 index 0000000..7715527 --- /dev/null +++ b/src/datasource/components/timeRangeDuration.test.ts @@ -0,0 +1,135 @@ +import { TimeRange, dateTime } from '@grafana/data'; +import { formatDuration, getTimeRangeDurationDays, isTimeRangeLarge } from './timeRangeDuration'; + +function createTimeRange(from: string, to: string): TimeRange { + const fromDate = dateTime(from); + const toDate = dateTime(to); + return { + from: fromDate, + to: toDate, + raw: { + from: fromDate, + to: toDate, + }, + }; +} + +describe('timeRangeDuration', () => { + describe('getTimeRangeDurationDays', () => { + it('should calculate duration correctly for 1 day', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-01-02T00:00:00Z'); + + const result = getTimeRangeDurationDays(timeRange); + expect(result).toBeCloseTo(1, 5); + }); + + it('should calculate duration correctly for 7 days', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-01-08T00:00:00Z'); + + const result = getTimeRangeDurationDays(timeRange); + expect(result).toBeCloseTo(7, 5); + }); + + it('should calculate duration correctly for 365 days', () => { + const timeRange = createTimeRange('2023-01-01T00:00:00Z', '2024-01-01T00:00:00Z'); + + const result = getTimeRangeDurationDays(timeRange); + expect(result).toBeCloseTo(365, 5); + }); + + it('should calculate duration correctly for partial days', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-01-01T12:00:00Z'); // 12 hours = 0.5 days + + const result = getTimeRangeDurationDays(timeRange); + expect(result).toBeCloseTo(0.5, 5); + }); + + it('should handle zero duration', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z'); + + const result = getTimeRangeDurationDays(timeRange); + expect(result).toBe(0); + }); + }); + + describe('isTimeRangeLarge', () => { + it('should return true when duration equals threshold', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2025-01-01T00:00:00Z'); // 365 days + + const result = isTimeRangeLarge(timeRange, 365); + expect(result).toBe(true); + }); + + it('should return true when duration exceeds threshold', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2025-06-01T00:00:00Z'); // ~516 days + + const result = isTimeRangeLarge(timeRange, 365); + expect(result).toBe(true); + }); + + it('should return false when duration is below threshold', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-12-30T00:00:00Z'); // 364 days + + const result = isTimeRangeLarge(timeRange, 365); + expect(result).toBe(false); + }); + + it('should use default threshold when not specified', () => { + const timeRange = createTimeRange('2023-01-01T00:00:00Z', '2024-01-01T00:00:00Z'); // 365 days + + const result = isTimeRangeLarge(timeRange); + expect(result).toBe(true); + }); + + it('should use custom threshold when specified', () => { + const timeRange = createTimeRange('2024-01-01T00:00:00Z', '2024-01-08T00:00:00Z'); // 7 days + + const result = isTimeRangeLarge(timeRange, 7); + expect(result).toBe(true); + }); + }); + + describe('formatDuration', () => { + it('should format single day correctly', () => { + expect(formatDuration(1)).toBe('1 days'); + }); + + it('should format multiple days correctly', () => { + expect(formatDuration(7)).toBe('7 days'); + expect(formatDuration(30)).toBe('30 days'); + expect(formatDuration(364)).toBe('364 days'); + }); + + it('should format exactly 365 days as 1 year', () => { + expect(formatDuration(365)).toBe('1 year'); + }); + + it('should format multiple years correctly', () => { + expect(formatDuration(730)).toBe('2 years'); + expect(formatDuration(1095)).toBe('3 years'); + }); + + it('should format years with remaining days correctly', () => { + expect(formatDuration(366)).toBe('1 year and 1 day'); + expect(formatDuration(370)).toBe('1 year and 5 days'); + expect(formatDuration(730)).toBe('2 years'); + expect(formatDuration(731)).toBe('2 years and 1 day'); + expect(formatDuration(800)).toBe('2 years and 70 days'); + }); + + it('should handle fractional days by flooring', () => { + expect(formatDuration(1.9)).toBe('1 days'); + expect(formatDuration(365.9)).toBe('1 year'); + expect(formatDuration(366.5)).toBe('1 year and 1 day'); + }); + + it('should handle zero days', () => { + expect(formatDuration(0)).toBe('0 days'); + }); + + it('should handle very large durations', () => { + expect(formatDuration(3650)).toBe('10 years'); + expect(formatDuration(3651)).toBe('10 years and 1 day'); + }); + }); +}); diff --git a/src/datasource/components/timeRangeDuration.ts b/src/datasource/components/timeRangeDuration.ts new file mode 100644 index 0000000..141e750 --- /dev/null +++ b/src/datasource/components/timeRangeDuration.ts @@ -0,0 +1,36 @@ +import { TimeRange } from '@grafana/data'; +import { TIME_RANGE_WARNING_THRESHOLD_DAYS } from '../constants'; + +export function getTimeRangeDurationDays(timeRange: TimeRange): number { + const from = timeRange.from.valueOf(); + const to = timeRange.to.valueOf(); + const durationMs = to - from; + return durationMs / (1000 * 60 * 60 * 24); +} + +export function isTimeRangeLarge( + timeRange: TimeRange, + thresholdDays: number = TIME_RANGE_WARNING_THRESHOLD_DAYS +): boolean { + return getTimeRangeDurationDays(timeRange) >= thresholdDays; +} + +/** + * Formats a duration in days as a human-readable string. + * Formats durations >= 365 days as years and days, otherwise as days only. + * Based on Zabbix best practices: + * - Zabbix "Max period for time selector" defaults to 2 years (range: 1-10 years) + * See: https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/administration/general + * Our limit are conservative to ensure good performance + */ +export function formatDuration(days: number): string { + if (days >= TIME_RANGE_WARNING_THRESHOLD_DAYS) { + const years = Math.floor(days / TIME_RANGE_WARNING_THRESHOLD_DAYS); + const remainingDays = Math.floor(days % TIME_RANGE_WARNING_THRESHOLD_DAYS); + if (remainingDays > 0) { + return `${years} year${years > 1 ? 's' : ''} and ${remainingDays} day${remainingDays > 1 ? 's' : ''}`; + } + return `${years} year${years > 1 ? 's' : ''}`; + } + return `${Math.floor(days)} days`; +} diff --git a/src/datasource/constants.ts b/src/datasource/constants.ts index 29d4e98..b40393e 100644 --- a/src/datasource/constants.ts +++ b/src/datasource/constants.ts @@ -51,3 +51,5 @@ export const RANGE_VARIABLE_VALUE = 'range_series'; export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001; export const ITEM_COUNT_WARNING_THRESHOLD = 500; + +export const TIME_RANGE_WARNING_THRESHOLD_DAYS = 365;