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:

<img width="705" height="374" alt="grafik"
src="https://github.com/user-attachments/assets/eb0ace4b-524a-488e-8f88-b7e9523660b0"
/>

## 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.
This commit is contained in:
ismail simsek
2025-12-30 16:08:46 +01:00
committed by GitHub
parent 3d631aedd7
commit ef9557f60e
5 changed files with 204 additions and 1 deletions

View File

@@ -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<ComboboxOption<QueryType>> = [
{
@@ -113,7 +114,7 @@ function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
export interface ZabbixQueryEditorProps
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
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 (
<Stack direction="column">
{queryType === c.MODE_METRICS && <ItemCountWarning itemCount={itemCount} />}
<TimeRangeWarning timeRange={range} />
<QueryEditorRow>
<InlineField label="Query type" labelWidth={12}>
<Combobox<QueryType>

View File

@@ -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<TimeRangeWarningProps> = ({ timeRange }) => {
if (!timeRange || !isTimeRangeLarge(timeRange)) {
return null;
}
const durationDays = getTimeRangeDurationDays(timeRange);
const formattedDuration = formatDuration(durationDays);
return (
<Alert title="Large time range" severity="warning">
Selected time range is {formattedDuration}. This query may return a large amount of data and could take longer to
execute.
</Alert>
);
};

View File

@@ -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');
});
});
});

View File

@@ -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`;
}

View File

@@ -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;