mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Support an auto refresh interval that is based on the query range (#70479)
This commit is contained in:
parent
2233886f80
commit
698b07518a
@ -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"]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<Props> {
|
||||
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<Props> {
|
||||
}
|
||||
|
||||
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<Props> {
|
||||
}
|
||||
|
||||
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<string>
|
||||
> {
|
||||
export function intervalsToOptions({
|
||||
intervals = defaultIntervals,
|
||||
showAutoInterval = false,
|
||||
}: { intervals?: string[]; showAutoInterval?: boolean } = {}): Array<SelectableValue<string>> {
|
||||
const options: Array<SelectableValue<string>> = 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;
|
||||
}
|
||||
|
@ -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<CurrentUserInternal, 'lightTheme'> {
|
||||
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);
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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<Props> {
|
||||
const fiscalYearStartMonth = dashboard.fiscalYearStartMonth;
|
||||
const hideIntervalPicker = dashboard.panelInEdit?.isEditing;
|
||||
|
||||
let text: string | undefined = undefined;
|
||||
if (dashboard.refresh === AutoRefreshInterval) {
|
||||
text = getTimeSrv().getAutoRefreshInteval().interval;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimePickerWithHistory
|
||||
@ -109,6 +115,8 @@ export class DashNavTimeControls extends Component<Props> {
|
||||
isOnCanvas={isOnCanvas}
|
||||
tooltip={t('dashboard.toolbar.refresh', 'Refresh dashboard')}
|
||||
noIntervalPicker={hideIntervalPicker}
|
||||
showAutoInterval={true}
|
||||
text={text}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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', () => {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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ę"
|
||||
|
@ -404,6 +404,10 @@
|
||||
"choose-interval": "自动刷新已关闭。选择刷新时间间隔",
|
||||
"duration-selected": "以当前所选间隔 {{durationAriaLabel}} 选择刷新时间间隔"
|
||||
},
|
||||
"auto-option": {
|
||||
"aria-label": "",
|
||||
"label": ""
|
||||
},
|
||||
"live-option": {
|
||||
"aria-label": "开启直播流",
|
||||
"label": "直播"
|
||||
|
Loading…
Reference in New Issue
Block a user