diff --git a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md index 474320e361d..44b20434a0d 100644 --- a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md +++ b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md @@ -136,6 +136,18 @@ The grid has a negative gravity that moves panels up if there is empty space abo "now": true, "hidden": false, "nowDelay": "", + "quick_ranges": [ + { + "display": "Last 6 hours" + "from": "now-6h", + "to": "now" + }, + { + "display": "Last 7 days" + "from": "now-7d", + "to": "now" + } + ], "refresh_intervals": [ "5s", "10s", @@ -163,6 +175,7 @@ Usage of the fields is explained below: | **now** | | | **hidden** | whether timepicker is hidden or not | | **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. | +| **quick_ranges** | custom quick ranges | | **refresh_intervals** | interval options available in the refresh picker dropdown | | **status** | | | **type** | | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 9ed6b5cc0c1..da8be7ef3d5 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -459,6 +459,13 @@ lineage: schemas: [{ options: _ } @cuetsy(kind="interface") @grafana(TSVeneer="type") + // Counterpart for TypeScript's TimeOption type. + #TimeOption: { + display: string + from: string + to: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + // Time picker configuration // It defines the default config for the time picker and the refresh picker for the specific dashboard. #TimePickerConfig: { @@ -468,6 +475,8 @@ lineage: schemas: [{ refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. time_options?: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + // Quick ranges for time picker. + quick_ranges?: [...#TimeOption] // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. nowDelay?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index d984321c532..3168e24dc6f 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -1,5 +1,3 @@ -import { each } from 'lodash'; - import { RawTimeRange, TimeRange, TimeZone, IntervalValues, RelativeTimeRange, TimeOption } from '../types/time'; import * as dateMath from './datemath'; @@ -17,7 +15,7 @@ const spans: { [key: string]: { display: string; section?: number } } = { y: { display: 'year' }, }; -const rangeOptions: TimeOption[] = [ +const BASE_RANGE_OPTIONS: TimeOption[] = [ { from: 'now/d', to: 'now/d', display: 'Today' }, { from: 'now/d', to: 'now', display: 'Today so far' }, { from: 'now/w', to: 'now/w', display: 'This week' }, @@ -66,7 +64,7 @@ const rangeOptions: TimeOption[] = [ { from: 'now/fy', to: 'now/fy', display: 'This fiscal year' }, ]; -const hiddenRangeOptions: TimeOption[] = [ +const HIDDEN_RANGE_OPTIONS: TimeOption[] = [ { from: 'now', to: 'now+1m', display: 'Next minute' }, { from: 'now', to: 'now+5m', display: 'Next 5 minutes' }, { from: 'now', to: 'now+15m', display: 'Next 15 minutes' }, @@ -86,13 +84,11 @@ const hiddenRangeOptions: TimeOption[] = [ { from: 'now', to: 'now+5y', display: 'Next 5 years' }, ]; -const rangeIndex: Record = {}; -each(rangeOptions, (frame) => { - rangeIndex[frame.from + ' to ' + frame.to] = frame; -}); -each(hiddenRangeOptions, (frame) => { - rangeIndex[frame.from + ' to ' + frame.to] = frame; -}); +const STANDARD_RANGE_OPTIONS = BASE_RANGE_OPTIONS.concat(HIDDEN_RANGE_OPTIONS); + +function findRangeInOptions(range: RawTimeRange, options: TimeOption[]) { + return options.find((option) => option.from === range.from && option.to === range.to); +} // handles expressions like // 5m @@ -106,7 +102,7 @@ export function describeTextRange(expr: string): TimeOption { expr = (isLast ? 'now-' : 'now') + expr; } - let opt = rangeIndex[expr + ' to now']; + let opt = findRangeInOptions({ from: expr, to: 'now' }, STANDARD_RANGE_OPTIONS); if (opt) { return opt; } @@ -141,17 +137,15 @@ export function describeTextRange(expr: string): TimeOption { /** * Use this function to get a properly formatted string representation of a {@link @grafana/data:RawTimeRange | range}. * - * @example - * ``` - * // Prints "2": - * console.log(add(1,1)); - * ``` * @category TimeUtils * @param range - a time range (usually specified by the TimePicker) + * @param timeZone - optional time zone. + * @param quickRanges - optional dashboard's custom quick ranges to pick range names from. * @alpha */ -export function describeTimeRange(range: RawTimeRange, timeZone?: TimeZone): string { - const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()]; +export function describeTimeRange(range: RawTimeRange, timeZone?: TimeZone, quickRanges?: TimeOption[]): string { + const rangeOptions = quickRanges ? quickRanges.concat(STANDARD_RANGE_OPTIONS) : STANDARD_RANGE_OPTIONS; + const option = findRangeInOptions(range, rangeOptions); if (option) { return option.display; diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 2e02d765559..d1d95f5c9ae 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -73,6 +73,7 @@ export type { VariableModel, DataSourceRef, DataTransformerConfig, + TimeOption, TimePickerConfig, Panel, FieldConfigSource, diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 2a0a2de17ba..e200106086a 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -651,6 +651,15 @@ export interface DataTransformerConfig { topic?: ('series' | 'annotations' | 'alertStates'); // replaced with common.DataTopic } +/** + * Counterpart for TypeScript's TimeOption type. + */ +export interface TimeOption { + display: string; + from: string; + to: string; +} + /** * Time picker configuration * It defines the default config for the time picker and the refresh picker for the specific dashboard. @@ -664,6 +673,10 @@ export interface TimePickerConfig { * Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. */ nowDelay?: string; + /** + * Quick ranges for time picker. + */ + quick_ranges?: Array; /** * Interval options available in the refresh picker dropdown. */ @@ -676,6 +689,7 @@ export interface TimePickerConfig { export const defaultTimePickerConfig: Partial = { hidden: false, + quick_ranges: [], refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], }; diff --git a/packages/grafana-schema/src/veneer/dashboard.types.ts b/packages/grafana-schema/src/veneer/dashboard.types.ts index 9976f4dec90..ee156beebbe 100644 --- a/packages/grafana-schema/src/veneer/dashboard.types.ts +++ b/packages/grafana-schema/src/veneer/dashboard.types.ts @@ -62,6 +62,8 @@ export interface DataTransformerConfig extends raw.DataTransform topic?: DataTopic; } +export interface TimeOption extends raw.TimeOption {} + export interface TimePickerConfig extends raw.TimePickerConfig {} export const defaultDashboard = raw.defaultDashboard as Dashboard; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 5b47ac1e84e..0f9e1e5b423 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -9,6 +9,7 @@ import { GrafanaTheme2, dateTimeFormat, timeZoneFormatUserFriendly, + TimeOption, TimeRange, TimeZone, dateMath, @@ -55,6 +56,7 @@ export interface TimeRangePickerProps { onZoom: () => void; onError?: (error?: string) => void; history?: TimeRange[]; + quickRanges?: TimeOption[]; hideQuickRanges?: boolean; widthOverride?: number; isOnCanvas?: boolean; @@ -81,6 +83,7 @@ export function TimeRangePicker(props: TimeRangePickerProps) { history, onChangeTimeZone, onChangeFiscalYearStartMonth, + quickRanges, hideQuickRanges, widthOverride, isOnCanvas, @@ -141,7 +144,7 @@ export function TimeRangePicker(props: TimeRangePickerProps) { const isFromAfterTo = value?.to?.isBefore(value.from); const timePickerIcon = isFromAfterTo ? 'exclamation-triangle' : 'clock-nine'; - const currentTimeRange = formattedRange(value, timeZone); + const currentTimeRange = formattedRange(value, timeZone, quickRanges); return ( @@ -187,7 +190,7 @@ export function TimeRangePicker(props: TimeRangePickerProps) { fiscalYearStartMonth={fiscalYearStartMonth} value={value} onChange={onChange} - quickOptions={quickOptions} + quickOptions={quickRanges || quickOptions} history={history} showHistory widthOverride={widthOverride} @@ -255,9 +258,9 @@ export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRang ); }; -type LabelProps = Pick; +type LabelProps = Pick; -export const TimePickerButtonLabel = memo(({ hideText, value, timeZone }) => { +export const TimePickerButtonLabel = memo(({ hideText, value, timeZone, quickRanges }) => { const styles = useStyles2(getLabelStyles); if (hideText) { @@ -266,7 +269,7 @@ export const TimePickerButtonLabel = memo(({ hideText, value, timeZo return ( - {formattedRange(value, timeZone)} + {formattedRange(value, timeZone, quickRanges)} {rangeUtil.describeTimeRangeAbbreviation(value, timeZone)} ); @@ -274,12 +277,12 @@ export const TimePickerButtonLabel = memo(({ hideText, value, timeZo TimePickerButtonLabel.displayName = 'TimePickerButtonLabel'; -const formattedRange = (value: TimeRange, timeZone?: TimeZone) => { +const formattedRange = (value: TimeRange, timeZone?: TimeZone, quickRanges?: TimeOption[]) => { const adjustedTimeRange = { to: dateMath.isMathString(value.raw.to) ? value.raw.to : value.to, from: dateMath.isMathString(value.raw.from) ? value.raw.from : value.from, }; - return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone); + return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone, quickRanges); }; const getStyles = (theme: GrafanaTheme2) => { diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 3d11277ab73..38e17ae6219 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -29,6 +29,18 @@ const ( DashboardCursorSyncTooltip DashboardCursorSync = 2 ) +// Counterpart for TypeScript's TimeOption type. +type TimeOption struct { + Display string `json:"display"` + From string `json:"from"` + To string `json:"to"` +} + +// NewTimeOption creates a new TimeOption object. +func NewTimeOption() *TimeOption { + return &TimeOption{} +} + // Time picker configuration // It defines the default config for the time picker and the refresh picker for the specific dashboard. type TimePickerConfig struct { @@ -38,6 +50,8 @@ type TimePickerConfig struct { RefreshIntervals []string `json:"refresh_intervals,omitempty"` // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. TimeOptions []string `json:"time_options,omitempty"` + // Quick ranges for time picker. + QuickRanges []TimeOption `json:"quick_ranges,omitempty"` // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. NowDelay *string `json:"nowDelay,omitempty"` } diff --git a/public/app/core/specs/rangeutil.test.ts b/public/app/core/specs/rangeutil.test.ts index 2f64c3c4ed6..27dd33ce142 100644 --- a/public/app/core/specs/rangeutil.test.ts +++ b/public/app/core/specs/rangeutil.test.ts @@ -1,4 +1,4 @@ -import { rangeUtil, dateTime } from '@grafana/data'; +import { rangeUtil, dateTime, TimeOption } from '@grafana/data'; describe('rangeUtil', () => { describe('Can get range text described', () => { @@ -57,6 +57,11 @@ describe('rangeUtil', () => { expect(text).toBe('now/d+6h to now'); }); + it('matches hidden time ranges', () => { + const text = rangeUtil.describeTimeRange({ from: 'now', to: 'now+30m' }); + expect(text).toBe('Next 30 minutes'); + }); + it('Date range with absolute to now', () => { const text = rangeUtil.describeTimeRange({ from: dateTime([2014, 10, 10, 2, 3, 4]), @@ -103,5 +108,17 @@ describe('rangeUtil', () => { const text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now+1h' }); expect(text).toBe('now-6h to now+1h'); }); + + it('Date range that is in custom quick ranges', () => { + const opt: TimeOption = { from: 'now-4w/w', to: 'now-1w/w', display: 'Previous 4 weeks' }; + const text = rangeUtil.describeTimeRange({ from: opt.from, to: opt.to }, undefined, [opt]); + expect(text).toBe('Previous 4 weeks'); + }); + + it('Date range description from custom quick ranges has higher priority', () => { + const opt: TimeOption = { from: 'now/d', to: 'now/d', display: 'This day' }; + const text = rangeUtil.describeTimeRange({ from: opt.from, to: opt.to }, undefined, [opt]); + expect(text).toBe('This day'); // overrides 'Today' + }); }); }); diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index ae1ced14a10..7be88761ec9 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -93,7 +93,7 @@ export class DashNavTimeControls extends Component { render() { const { dashboard, isOnCanvas } = this.props; - const { refresh_intervals } = dashboard.timepicker; + const { quick_ranges, refresh_intervals } = dashboard.timepicker; const intervals = getTimeSrv().getValidIntervals(refresh_intervals || defaultIntervals); const timePickerValue = getTimeSrv().timeRange(); @@ -122,6 +122,7 @@ export class DashNavTimeControls extends Component { isOnCanvas={isOnCanvas} onToolbarTimePickerClick={this.props.onToolbarTimePickerClick} weekStart={weekStart} + quickRanges={quick_ranges} />