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;