diff --git a/.betterer.results b/.betterer.results index e496b693a5d..07bb4a249ff 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2242,8 +2242,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard/services/TimeSrv.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard/state/DashboardMigrator.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -2399,8 +2398,7 @@ exports[`better eslint`] = { ], "public/app/features/dashboard/state/TimeModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/dashboard/state/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/docs/sources/dashboards/use-dashboards/index.md b/docs/sources/dashboards/use-dashboards/index.md index 7464dbb7756..4d3fd3b6cdf 100644 --- a/docs/sources/dashboards/use-dashboards/index.md +++ b/docs/sources/dashboards/use-dashboards/index.md @@ -196,7 +196,9 @@ Click and drag to select the time range in the visualization that you want to vi Click the **Refresh dashboard** icon to immediately run every query on the dashboard and refresh the visualizations. Grafana cancels any pending requests when you trigger a refresh. -By default, Grafana does not automatically refresh the dashboard. Queries run on their own schedule according to the panel settings. However, if you want to regularly refresh the dashboard, then click the down arrow next to the **Refresh dashboard** icon and then select a refresh interval. +By default, Grafana does not automatically refresh the dashboard. Queries run on their own schedule according to the panel settings. However, if you want to regularly refresh the dashboard, click the down arrow next to the **Refresh dashboard** icon, and then select a refresh interval. + +Selecting the **Auto** interval schedules a refresh based on the query time range and browser window width. Short time ranges update frequently, while longer ones update infrequently. There is no need to refresh more often then the pixels available to draw any updates. ### Control the time range using a URL diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx index e2555222657..278a85a3137 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx @@ -22,6 +22,7 @@ export interface Props { isLive?: boolean; text?: string; noIntervalPicker?: boolean; + showAutoInterval?: boolean; width?: string; primary?: boolean; isOnCanvas?: boolean; @@ -38,6 +39,11 @@ export class RefreshPicker extends PureComponent { value: 'LIVE', ariaLabel: 'Turn on live streaming', }; + static autoOption = { + label: 'Auto', + value: 'auto', + ariaLabel: 'Select refresh from the query range', + }; static isLive = (refreshInterval?: string): boolean => refreshInterval === RefreshPicker.liveOption.value; @@ -69,11 +75,12 @@ export class RefreshPicker extends PureComponent { } render() { - const { onRefresh, intervals, tooltip, value, text, isLoading, noIntervalPicker, width } = this.props; + const { onRefresh, intervals, tooltip, value, text, isLoading, noIntervalPicker, width, showAutoInterval } = + this.props; const currentValue = value || ''; const variant = this.getVariant(); - const options = intervalsToOptions({ intervals }); + const options = intervalsToOptions({ intervals, showAutoInterval }); const option = options.find(({ value }) => value === currentValue); const translatedOffOption = translateOption(RefreshPicker.offOption.value); let selectedValue = option || translatedOffOption; @@ -124,23 +131,36 @@ export class RefreshPicker extends PureComponent { } export function translateOption(option: string) { - if (option === RefreshPicker.liveOption.value) { - return { - label: t('refresh-picker.live-option.label', 'Live'), - value: 'LIVE', - ariaLabel: t('refresh-picker.live-option.aria-label', 'Turn on live streaming'), - }; + switch (option) { + case RefreshPicker.liveOption.value: + return { + label: t('refresh-picker.live-option.label', 'Live'), + value: option, + ariaLabel: t('refresh-picker.live-option.aria-label', 'Turn on live streaming'), + }; + case RefreshPicker.offOption.value: + return { + label: t('refresh-picker.off-option.label', 'Off'), + value: option, + ariaLabel: t('refresh-picker.off-option.aria-label', 'Turn off auto refresh'), + }; + case RefreshPicker.autoOption.value: + return { + label: t('refresh-picker.auto-option.label', RefreshPicker.autoOption.label), + value: option, + ariaLabel: t('refresh-picker.auto-option.aria-label', RefreshPicker.autoOption.ariaLabel), + }; } return { - label: t('refresh-picker.off-option.label', 'Off'), - value: '', - ariaLabel: t('refresh-picker.off-option.aria-label', 'Turn off auto refresh'), + label: option, + value: option, }; } -export function intervalsToOptions({ intervals = defaultIntervals }: { intervals?: string[] } = {}): Array< - SelectableValue -> { +export function intervalsToOptions({ + intervals = defaultIntervals, + showAutoInterval = false, +}: { intervals?: string[]; showAutoInterval?: boolean } = {}): Array> { const options: Array> = intervals.map((interval) => { const duration = parseDuration(interval); const ariaLabel = formatDuration(duration); @@ -152,6 +172,9 @@ export function intervalsToOptions({ intervals = defaultIntervals }: { intervals }; }); + if (showAutoInterval) { + options.unshift(translateOption(RefreshPicker.autoOption.value)); + } options.unshift(translateOption(RefreshPicker.offOption.value)); return options; } diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 4e792a2fcdd..52ba18b85ae 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -7,6 +7,10 @@ import { CurrentUserInternal } from 'app/types/config'; import config from '../../core/config'; +// When set to auto, the interval will be based on the query range +// NOTE: this is defined here rather than TimeSrv so we avoid circular dependencies +export const AutoRefreshInterval = 'auto'; + export class User implements Omit { isSignedIn: boolean; id: number; @@ -154,7 +158,7 @@ export class ContextSrv { // checks whether the passed interval is longer than the configured minimum refresh rate isAllowedInterval(interval: string) { - if (!config.minRefreshInterval) { + if (!config.minRefreshInterval || interval === AutoRefreshInterval) { return true; } return rangeUtil.intervalToMs(interval) >= rangeUtil.intervalToMs(config.minRefreshInterval); diff --git a/public/app/core/utils/query.test.ts b/public/app/core/utils/query.test.ts index efd5f1944bf..c0dc74e2af7 100644 --- a/public/app/core/utils/query.test.ts +++ b/public/app/core/utils/query.test.ts @@ -25,7 +25,7 @@ describe('Get next refId char', () => { expect(getNextRefIdChar([])).toEqual('A'); }); - it('should get the first avaliable character if a query has been deleted out of order', () => { + it('should get the first available character if a query has been deleted out of order', () => { expect(getNextRefIdChar(outOfOrderDataQuery)).toEqual('C'); }); diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 66743294ca7..532feb98936 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -7,6 +7,7 @@ import { defaultIntervals, RefreshPicker } from '@grafana/ui'; import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory'; import { appEvents } from 'app/core/core'; import { t } from 'app/core/internationalization'; +import { AutoRefreshInterval } from 'app/core/services/context_srv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../../types/events'; @@ -87,6 +88,11 @@ export class DashNavTimeControls extends Component { const fiscalYearStartMonth = dashboard.fiscalYearStartMonth; const hideIntervalPicker = dashboard.panelInEdit?.isEditing; + let text: string | undefined = undefined; + if (dashboard.refresh === AutoRefreshInterval) { + text = getTimeSrv().getAutoRefreshInteval().interval; + } + return ( <> { isOnCanvas={isOnCanvas} tooltip={t('dashboard.toolbar.refresh', 'Refresh dashboard')} noIntervalPicker={hideIntervalPicker} + showAutoInterval={true} + text={text} /> ); diff --git a/public/app/features/dashboard/services/TimeSrv.test.ts b/public/app/features/dashboard/services/TimeSrv.test.ts index fb6a20f19c4..4d7a389d217 100644 --- a/public/app/features/dashboard/services/TimeSrv.test.ts +++ b/public/app/features/dashboard/services/TimeSrv.test.ts @@ -82,7 +82,7 @@ describe('timeSrv', () => { timeSrv = new TimeSrv(new ContextSrvStub()); // dashboard saved with refresh on - _dashboard.refresh = true; + _dashboard.refresh = '10s'; timeSrv.init(_dashboard); expect(timeSrv.refresh).toBe(false); @@ -298,6 +298,11 @@ describe('timeSrv', () => { timeSrv.resumeAutoRefresh(); expect(timeSrv.refreshTimer).not.toBeUndefined(); }); + + it('should allow an auto refresh value', () => { + timeSrv.setAutoRefresh('auto'); + expect(timeSrv.refreshTimer).not.toBeUndefined(); + }); }); describe('isRefreshOutsideThreshold', () => { diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 94e13f3de82..b3df6f21736 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -9,11 +9,12 @@ import { RawTimeRange, TimeRange, toUtc, + IntervalValues, } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import { config } from 'app/core/config'; -import { contextSrv, ContextSrv } from 'app/core/services/context_srv'; +import { AutoRefreshInterval, contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; @@ -24,11 +25,12 @@ import { getRefreshFromUrl } from '../utils/getRefreshFromUrl'; export class TimeSrv { time: RawTimeRange; refreshTimer: number | undefined; - refresh: any; - oldRefresh: string | null | undefined; + refresh?: string | false; + oldRefresh?: string; timeModel?: TimeModel; timeAtLoad: RawTimeRange; private autoRefreshBlocked?: boolean; + private refreshMS?: number; constructor(private contextSrv: ContextSrv) { // default time @@ -89,11 +91,10 @@ export class TimeSrv { } getValidIntervals(intervals: string[]): string[] { - if (!this.contextSrv.minRefreshInterval) { - return intervals; + if (this.contextSrv.minRefreshInterval) { + return intervals.filter((str) => str !== '').filter(this.contextSrv.isAllowedInterval); } - - return intervals.filter((str) => str !== '').filter(this.contextSrv.isAllowedInterval); + return intervals; } private parseTime() { @@ -214,7 +215,7 @@ export class TimeSrv { return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to); } - setAutoRefresh(interval: any) { + setAutoRefresh(interval: string | false) { if (this.timeModel) { this.timeModel.refresh = interval; } @@ -232,21 +233,35 @@ export class TimeSrv { return; } - const validInterval = this.contextSrv.getValidInterval(interval); - const intervalMs = rangeUtil.intervalToMs(validInterval); + let refresh = interval; + let intervalMs = 60 * 1000; + if (interval === AutoRefreshInterval) { + intervalMs = this.getAutoRefreshInteval().intervalMs; + } else { + refresh = this.contextSrv.getValidInterval(interval as string); + intervalMs = rangeUtil.intervalToMs(refresh); + } + this.refreshMS = intervalMs; this.refreshTimer = window.setTimeout(() => { this.startNextRefreshTimer(intervalMs); this.refreshTimeModel(); }, intervalMs); - const refresh = this.contextSrv.getValidInterval(interval); - if (currentUrlState.refresh !== refresh) { locationService.partial({ refresh }, true); } } + getAutoRefreshInteval(): IntervalValues { + const resolution = window?.innerWidth ?? 2000; + return rangeUtil.calculateInterval( + this.timeRange(), + resolution, // the max pixels possibles + config.minRefreshInterval + ); + } + refreshTimeModel() { this.timeModel?.timeRangeUpdated(this.timeRange()); } @@ -265,6 +280,7 @@ export class TimeSrv { stopAutoRefresh() { clearTimeout(this.refreshTimer); this.refreshTimer = undefined; + this.refreshMS = undefined; } // resume auto-refresh based on old dashboard refresh property @@ -283,7 +299,7 @@ export class TimeSrv { this.setAutoRefresh(false); } else if (this.oldRefresh && this.oldRefresh !== this.timeModel?.refresh) { this.setAutoRefresh(this.oldRefresh); - this.oldRefresh = null; + this.oldRefresh = undefined; } if (updateUrl === true) { @@ -300,6 +316,14 @@ export class TimeSrv { locationService.partial(urlParams); } + // Check if the auto refresh interval has changed + if (this.timeModel?.refresh === AutoRefreshInterval) { + const v = this.getAutoRefreshInteval().intervalMs; + if (v !== this.refreshMS) { + this.setAutoRefresh(AutoRefreshInterval); + } + } + this.refreshTimeModel(); } diff --git a/public/app/features/dashboard/state/TimeModel.ts b/public/app/features/dashboard/state/TimeModel.ts index b7df209d6f7..539f4299886 100644 --- a/public/app/features/dashboard/state/TimeModel.ts +++ b/public/app/features/dashboard/state/TimeModel.ts @@ -3,7 +3,7 @@ import { TimeRange, TimeZone } from '@grafana/data'; export interface TimeModel { time: any; fiscalYearStartMonth?: number; - refresh: any; + refresh?: string | false; timepicker: any; getTimezone(): TimeZone; timeRangeUpdated(timeRange: TimeRange): void; diff --git a/public/app/features/dashboard/utils/getRefreshFromUrl.ts b/public/app/features/dashboard/utils/getRefreshFromUrl.ts index 44ce399041a..cf6aac7c115 100644 --- a/public/app/features/dashboard/utils/getRefreshFromUrl.ts +++ b/public/app/features/dashboard/utils/getRefreshFromUrl.ts @@ -2,7 +2,7 @@ import { defaultIntervals } from '@grafana/ui'; interface Args { urlRefresh: string | null; - currentRefresh: string | boolean | undefined; + currentRefresh: string | false | undefined; isAllowedIntervalFn: (interval: string) => boolean; minRefreshInterval: string; refreshIntervals?: string[]; @@ -18,7 +18,7 @@ export function getRefreshFromUrl({ isAllowedIntervalFn, minRefreshInterval, refreshIntervals = defaultIntervals, -}: Args): string | boolean | undefined { +}: Args): string | false | undefined { if (!urlRefresh) { return currentRefresh; } diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index f0b8f8ee335..12e396bb045 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "Automatische Aktualisierung ausgeschaltet. Aktualisierungszeitintervall auswählen", "duration-selected": "Aktualisierungszeitintervall mit aktuellem Intervall {{durationAriaLabel}} ausgewählt" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "Live-Streaming einschalten", "label": "Live" diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8f6d90d4589..902cf5d6dbd 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "Auto refresh turned off. Choose refresh time interval", "duration-selected": "Choose refresh time interval with current interval {{durationAriaLabel}} selected" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "Turn on live streaming", "label": "Live" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index ed33eb3d887..710e387d951 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "Actualización automática desactivada. Elija un intervalo de tiempo de actualización", "duration-selected": "Elegir intervalo de tiempo de actualización con el intervalo actual {{durationAriaLabel}} seleccionado" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "Activar transmisión en directo", "label": "En directo" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 880d59b5a56..9c9246fee46 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "Actualisation automatique désactivée. Choisir un intervalle de temps d'actualisation", "duration-selected": "Choisir l'intervalle de temps d'actualisation avec l'intervalle actuel {{durationAriaLabel}} sélectionné" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "Activer la diffusion en direct", "label": "En direct" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 670ef64b0d7..194201b2c63 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "Åūŧő řęƒřęşĥ ŧūřʼnęđ őƒƒ. Cĥőőşę řęƒřęşĥ ŧįmę įʼnŧęřväľ", "duration-selected": "Cĥőőşę řęƒřęşĥ ŧįmę įʼnŧęřväľ ŵįŧĥ čūřřęʼnŧ įʼnŧęřväľ {{durationAriaLabel}} şęľęčŧęđ" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "Ŧūřʼn őʼn ľįvę şŧřęämįʼnģ", "label": "Ŀįvę" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 57e47dca102..c066f0cad29 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -404,6 +404,10 @@ "choose-interval": "自动刷新已关闭。选择刷新时间间隔", "duration-selected": "以当前所选间隔 {{durationAriaLabel}} 选择刷新时间间隔" }, + "auto-option": { + "aria-label": "", + "label": "" + }, "live-option": { "aria-label": "开启直播流", "label": "直播"