mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586)
* added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
standardEditorsRegistry,
|
||||
standardFieldConfigEditorRegistry,
|
||||
standardTransformersRegistry,
|
||||
setTimeZoneResolver,
|
||||
} from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
|
||||
@@ -93,6 +94,7 @@ export class GrafanaApp {
|
||||
const app = angular.module('grafana', []);
|
||||
|
||||
setLocale(config.bootData.user.locale);
|
||||
setTimeZoneResolver(() => config.bootData.user.timeZone);
|
||||
|
||||
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
|
||||
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
|
||||
import { SearchField, SearchResults, SearchWrapper, SearchResultsFilter } from '../features/search';
|
||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
react2AngularDirective('footer', Footer, []);
|
||||
@@ -151,7 +152,7 @@ export function registerAngularDirectives() {
|
||||
'items',
|
||||
['onClose', { watchDepth: 'reference', wrapApply: true }],
|
||||
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }],
|
||||
['formatSourceDate', { watchDepth: 'reference', wrapApply: true }],
|
||||
['timeZone', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
|
||||
// We keep the drilldown terminology here because of as using data-* directive
|
||||
@@ -201,4 +202,11 @@ export function registerAngularDirectives() {
|
||||
['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
react2AngularDirective('variableEditorContainer', VariableEditorContainer, []);
|
||||
react2AngularDirective('timePickerSettings', TimePickerSettings, [
|
||||
['getDashboard', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const { Select } = LegacyForms;
|
||||
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
resourceUri: string;
|
||||
@@ -23,12 +24,18 @@ const themes = [
|
||||
{ value: 'light', label: 'Light' },
|
||||
];
|
||||
|
||||
const timezones = [
|
||||
const grafanaTimeZones = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'browser', label: 'Local browser time' },
|
||||
{ value: 'utc', label: 'UTC' },
|
||||
];
|
||||
|
||||
const timeZones = getTimeZoneGroups().reduce((tzs, group) => {
|
||||
const options = group.options.map(tz => ({ value: tz, label: tz }));
|
||||
tzs.push.apply(tzs, options);
|
||||
return tzs;
|
||||
}, grafanaTimeZones);
|
||||
|
||||
export class SharedPreferences extends PureComponent<Props, State> {
|
||||
backendSrv = backendSrv;
|
||||
|
||||
@@ -91,12 +98,18 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
onThemeChanged = (theme: string) => {
|
||||
this.setState({ theme });
|
||||
onThemeChanged = (theme: SelectableValue<string>) => {
|
||||
if (!theme || !theme.value) {
|
||||
return;
|
||||
}
|
||||
this.setState({ theme: theme.value });
|
||||
};
|
||||
|
||||
onTimeZoneChanged = (timezone: string) => {
|
||||
this.setState({ timezone });
|
||||
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
||||
if (!timezone || typeof timezone.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
this.setState({ timezone: timezone.value });
|
||||
};
|
||||
|
||||
onHomeDashboardChanged = (dashboardId: number) => {
|
||||
@@ -122,7 +135,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
isSearchable={false}
|
||||
value={themes.find(item => item.value === theme)}
|
||||
options={themes}
|
||||
onChange={theme => this.onThemeChanged(theme.value)}
|
||||
onChange={this.onThemeChanged}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
@@ -146,10 +159,10 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label width-11">Timezone</label>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
value={timezones.find(item => item.value === timezone)}
|
||||
onChange={timezone => this.onTimeZoneChanged(timezone.value)}
|
||||
options={timezones}
|
||||
isSearchable={true}
|
||||
value={timeZones.find(item => item.value === timezone)}
|
||||
onChange={this.onTimeZoneChanged}
|
||||
options={timeZones}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
|
||||
import { TimeRange, isDateTime, dateTime } from '@grafana/data';
|
||||
import { TimeRange, isDateTime, toUtc } from '@grafana/data';
|
||||
import { Props as TimePickerProps, TimeRangePicker } from '@grafana/ui/src/components/TimePicker/TimeRangePicker';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';
|
||||
@@ -33,8 +33,8 @@ function convertIfJson(history: TimeRange[]): TimeRange[] {
|
||||
}
|
||||
|
||||
return {
|
||||
from: dateTime(time.from),
|
||||
to: dateTime(time.to),
|
||||
from: toUtc(time.from),
|
||||
to: toUtc(time.to),
|
||||
raw: time.raw,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
LogsMetaKind,
|
||||
LogsDedupStrategy,
|
||||
GraphSeriesXY,
|
||||
toUtc,
|
||||
dateTimeFormat,
|
||||
dateTimeFormatTimeAgo,
|
||||
NullValueMode,
|
||||
toDataFrame,
|
||||
FieldCache,
|
||||
@@ -250,8 +251,6 @@ function separateLogsAndMetrics(dataFrames: DataFrame[]) {
|
||||
return { logSeries, metricSeries };
|
||||
}
|
||||
|
||||
const logTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
interface LogFields {
|
||||
series: DataFrame;
|
||||
|
||||
@@ -333,10 +332,10 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
||||
rowIndex: j,
|
||||
dataFrame: series,
|
||||
logLevel,
|
||||
timeFromNow: time.fromNow(),
|
||||
timeFromNow: dateTimeFormatTimeAgo(ts),
|
||||
timeEpochMs: time.valueOf(),
|
||||
timeLocal: time.format(logTimeFormat),
|
||||
timeUtc: toUtc(time.valueOf()).format(logTimeFormat),
|
||||
timeLocal: dateTimeFormat(ts, { timeZone: 'browser' }),
|
||||
timeUtc: dateTimeFormat(ts, { timeZone: 'utc' }),
|
||||
uniqueLabels,
|
||||
hasAnsi,
|
||||
searchWords,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
dateMath,
|
||||
DefaultTimeZone,
|
||||
HistoryItem,
|
||||
IntervalValues,
|
||||
LogRowModel,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
toUtc,
|
||||
ExploreMode,
|
||||
urlUtil,
|
||||
DefaultTimeZone,
|
||||
} from '@grafana/data';
|
||||
import store from 'app/core/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
@@ -115,7 +115,8 @@ export function buildQueryTransaction(
|
||||
queries: DataQuery[],
|
||||
queryOptions: QueryOptions,
|
||||
range: TimeRange,
|
||||
scanning: boolean
|
||||
scanning: boolean,
|
||||
timeZone?: TimeZone
|
||||
): QueryTransaction {
|
||||
const configuredQueries = queries.map(query => ({ ...query, ...queryOptions }));
|
||||
const key = queries.reduce((combinedKey, query) => {
|
||||
@@ -135,7 +136,7 @@ export function buildQueryTransaction(
|
||||
app: CoreApp.Explore,
|
||||
dashboardId: 0,
|
||||
// TODO probably should be taken from preferences but does not seem to be used anyway.
|
||||
timezone: DefaultTimeZone,
|
||||
timezone: timeZone || DefaultTimeZone,
|
||||
startTime: Date.now(),
|
||||
interval,
|
||||
intervalMs,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & Utils
|
||||
import { DataQuery, DataSourceApi, ExploreMode, dateTime, AppEvents, urlUtil } from '@grafana/data';
|
||||
import { DataQuery, DataSourceApi, ExploreMode, dateTimeFormat, AppEvents, urlUtil } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import store from 'app/core/store';
|
||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
||||
@@ -237,7 +237,9 @@ export const createRetentionPeriodBoundary = (days: number, isLastTs: boolean) =
|
||||
};
|
||||
|
||||
export function createDateStringFromTs(ts: number) {
|
||||
return dateTime(ts).format('MMMM D');
|
||||
return dateTimeFormat(ts, {
|
||||
format: 'MMMM D',
|
||||
});
|
||||
}
|
||||
|
||||
export function getQueryDisplayText(query: DataQuery): string {
|
||||
|
||||
@@ -165,21 +165,21 @@ export function grafanaTimeFormat(ticks: number, min: number, max: number) {
|
||||
const oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return '%H:%M:%S';
|
||||
return 'HH:mm:ss';
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return '%H:%M';
|
||||
return 'HH:mm';
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return '%m/%d %H:%M';
|
||||
return 'MM/DD HH:mm';
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return '%m/%d';
|
||||
return 'MM/DD';
|
||||
}
|
||||
return '%Y-%m';
|
||||
return 'YYYY-MM';
|
||||
}
|
||||
|
||||
return '%H:%M';
|
||||
return 'HH:mm';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
import { NavModelSrv } from 'app/core/core';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormatTimeAgo, dateTimeFormat } from '@grafana/data';
|
||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||
|
||||
export default class AdminEditUserCtrl {
|
||||
@@ -47,8 +47,8 @@ export default class AdminEditUserCtrl {
|
||||
return {
|
||||
id: session.id,
|
||||
isActive: session.isActive,
|
||||
seenAt: dateTime(session.seenAt).fromNow(),
|
||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
||||
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||
clientIp: session.clientIp,
|
||||
browser: session.browser,
|
||||
browserVersion: session.browserVersion,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { SyncInfo, UserDTO } from 'app/types';
|
||||
import { Button, LinkButton } from '@grafana/ui';
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
interface State {}
|
||||
|
||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
const debugLDAPMappingBaseURL = '/admin/ldap';
|
||||
|
||||
export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
||||
@@ -21,9 +21,10 @@ export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { ldapSyncInfo, user } = this.props;
|
||||
const nextSyncTime = dateTime(ldapSyncInfo.nextSync).format(syncTimeFormat);
|
||||
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTime(ldapSyncInfo.prevSync.started).format(syncTimeFormat) : '';
|
||||
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
||||
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(ldapSyncInfo.prevSync.started, { format }) : '';
|
||||
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { LdapUserSyncInfo } from 'app/types';
|
||||
import { Icon } from '@grafana/ui';
|
||||
|
||||
@@ -13,7 +13,7 @@ interface State {
|
||||
isSyncing: boolean;
|
||||
}
|
||||
|
||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
|
||||
export class UserSyncInfo extends PureComponent<Props, State> {
|
||||
state = {
|
||||
@@ -35,9 +35,10 @@ export class UserSyncInfo extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { syncInfo, disableSync } = this.props;
|
||||
const { isSyncing } = this.state;
|
||||
const nextSyncTime = syncInfo.nextSync ? dateTime(syncInfo.nextSync).format(syncTimeFormat) : '';
|
||||
const nextSyncSuccessful = syncInfo && syncInfo.nextSync;
|
||||
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(syncInfo.nextSync, { format }) : '';
|
||||
const prevSyncSuccessful = syncInfo && syncInfo.prevSync;
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTime(syncInfo.prevSync).format(syncTimeFormat) : '';
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(syncInfo.prevSync, { format }) : '';
|
||||
const isDisabled = isSyncing || disableSync;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { SyncInfo } from 'app/types';
|
||||
|
||||
@@ -11,7 +11,7 @@ interface State {
|
||||
isSyncing: boolean;
|
||||
}
|
||||
|
||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
|
||||
export class LdapSyncInfo extends PureComponent<Props, State> {
|
||||
state = {
|
||||
@@ -26,9 +26,9 @@ export class LdapSyncInfo extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { ldapSyncInfo } = this.props;
|
||||
const { isSyncing } = this.state;
|
||||
const nextSyncTime = dateTime(ldapSyncInfo.nextSync).format(syncTimeFormat);
|
||||
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
|
||||
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTime(ldapSyncInfo.prevSync.started).format(syncTimeFormat) : '';
|
||||
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(ldapSyncInfo.prevSync!.started, { format }) : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import config from 'app/core/config';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
||||
|
||||
@@ -141,8 +141,8 @@ export function loadUserSessions(userId: number): ThunkResult<void> {
|
||||
return {
|
||||
id: session.id,
|
||||
isActive: session.isActive,
|
||||
seenAt: dateTime(session.seenAt).fromNow(),
|
||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
||||
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||
clientIp: session.clientIp,
|
||||
browser: session.browser,
|
||||
browserVersion: session.browserVersion,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
IconButton,
|
||||
} from '@grafana/ui';
|
||||
const { Input, Switch } = LegacyForms;
|
||||
import { dateTime, isDateTime, NavModel } from '@grafana/data';
|
||||
import { NavModel, dateTimeFormat } from '@grafana/data';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { store } from 'app/store/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
@@ -174,11 +174,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
if (!date) {
|
||||
return 'No expiration date';
|
||||
}
|
||||
date = isDateTime(date) ? date : dateTime(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
const timezone = getTimeZone(store.getState().user);
|
||||
|
||||
return timezone === 'utc' ? date.utc().format(format) : date.format(format);
|
||||
const timeZone = getTimeZone(store.getState().user);
|
||||
return dateTimeFormat(date, { format, timeZone });
|
||||
}
|
||||
|
||||
renderAddApiKeyForm() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { DashboardSrv } from '../../services/DashboardSrv';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { AppEvents, locationUtil } from '@grafana/data';
|
||||
import { AppEvents, locationUtil, TimeZone } from '@grafana/data';
|
||||
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
||||
|
||||
export class SettingsCtrl {
|
||||
@@ -255,6 +255,22 @@ export class SettingsCtrl {
|
||||
getDashboard = () => {
|
||||
return this.dashboard;
|
||||
};
|
||||
|
||||
onRefreshIntervalChange = (intervals: string[]) => {
|
||||
this.dashboard.timepicker.refresh_intervals = intervals;
|
||||
};
|
||||
|
||||
onNowDelayChange = (nowDelay: string) => {
|
||||
this.dashboard.timepicker.nowDelay = nowDelay;
|
||||
};
|
||||
|
||||
onHideTimePickerChange = (hide: boolean) => {
|
||||
this.dashboard.timepicker.hidden = hide;
|
||||
};
|
||||
|
||||
onTimeZoneChange = (timeZone: TimeZone) => {
|
||||
this.dashboard.timezone = timeZone;
|
||||
};
|
||||
}
|
||||
|
||||
export function dashboardSettings() {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { config } from 'app/core/config';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class TimePickerCtrl {
|
||||
panel: any;
|
||||
dashboard: DashboardModel;
|
||||
|
||||
constructor() {
|
||||
this.panel = this.dashboard.timepicker;
|
||||
this.panel.refresh_intervals = this.panel.refresh_intervals || [
|
||||
'5s',
|
||||
'10s',
|
||||
'30s',
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'2h',
|
||||
'1d',
|
||||
];
|
||||
if (config.minRefreshInterval) {
|
||||
this.panel.refresh_intervals = this.filterRefreshRates(this.panel.refresh_intervals);
|
||||
}
|
||||
}
|
||||
|
||||
filterRefreshRates(refreshRates: string[]) {
|
||||
return refreshRates.filter(rate => {
|
||||
return kbn.interval_to_ms(rate) > kbn.interval_to_ms(config.minRefreshInterval);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const template = `
|
||||
<div class="editor-row">
|
||||
<h5 class="section-heading">Time Options</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Timezone</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in
|
||||
[{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Auto-refresh</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Now delay now-</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay"
|
||||
placeholder="0m"
|
||||
valid-time-span
|
||||
bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
|
||||
data-placement="right">
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export function TimePickerSettings() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: TimePickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dashboard: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfTimePickerSettings', TimePickerSettings);
|
||||
@@ -0,0 +1,151 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Select, Input, Tooltip, LegacyForms } from '@grafana/ui';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { getTimeZoneGroups, TimeZone, rangeUtil, SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
const grafanaTimeZones = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'browser', label: 'Local browser time' },
|
||||
{ value: 'utc', label: 'UTC' },
|
||||
];
|
||||
|
||||
const timeZones = getTimeZoneGroups().reduce((tzs, group) => {
|
||||
const options = group.options.map(tz => ({ value: tz, label: tz }));
|
||||
tzs.push.apply(tzs, options);
|
||||
return tzs;
|
||||
}, grafanaTimeZones);
|
||||
|
||||
interface Props {
|
||||
getDashboard: () => DashboardModel;
|
||||
onTimeZoneChange: (timeZone: TimeZone) => void;
|
||||
onRefreshIntervalChange: (interval: string[]) => void;
|
||||
onNowDelayChange: (nowDelay: string) => void;
|
||||
onHideTimePickerChange: (hide: boolean) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isNowDelayValid: boolean;
|
||||
}
|
||||
|
||||
export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
state: State = { isNowDelayValid: true };
|
||||
|
||||
componentDidMount() {
|
||||
const { timepicker } = this.props.getDashboard();
|
||||
let intervals: string[] = timepicker.refresh_intervals ?? [
|
||||
'5s',
|
||||
'10s',
|
||||
'30s',
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'2h',
|
||||
'1d',
|
||||
];
|
||||
|
||||
if (config.minRefreshInterval) {
|
||||
intervals = intervals.filter(rate => {
|
||||
return kbn.interval_to_ms(rate) > kbn.interval_to_ms(config.minRefreshInterval);
|
||||
});
|
||||
}
|
||||
|
||||
this.props.onRefreshIntervalChange(intervals);
|
||||
}
|
||||
|
||||
getRefreshIntervals = () => {
|
||||
const dashboard = this.props.getDashboard();
|
||||
if (!Array.isArray(dashboard.timepicker.refresh_intervals)) {
|
||||
return '';
|
||||
}
|
||||
return dashboard.timepicker.refresh_intervals.join(',');
|
||||
};
|
||||
|
||||
onRefreshIntervalChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
if (!event.currentTarget.value) {
|
||||
return;
|
||||
}
|
||||
const intervals = event.currentTarget.value.split(',');
|
||||
this.props.onRefreshIntervalChange(intervals);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onNowDelayChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
if (isEmpty(value)) {
|
||||
this.setState({ isNowDelayValid: true });
|
||||
return this.props.onNowDelayChange(value);
|
||||
}
|
||||
|
||||
if (rangeUtil.isValidTimeSpan(value)) {
|
||||
this.setState({ isNowDelayValid: true });
|
||||
return this.props.onNowDelayChange(value);
|
||||
}
|
||||
|
||||
this.setState({ isNowDelayValid: false });
|
||||
};
|
||||
|
||||
onHideTimePickerChange = () => {
|
||||
const dashboard = this.props.getDashboard();
|
||||
this.props.onHideTimePickerChange(!dashboard.timepicker.hidden);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onTimeZoneChange = (timeZone: SelectableValue<string>) => {
|
||||
if (!timeZone || typeof timeZone.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
this.props.onTimeZoneChange(timeZone.value);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
const dashboard = this.props.getDashboard();
|
||||
const value = timeZones.find(item => item.value === dashboard.timezone);
|
||||
|
||||
return (
|
||||
<div className="editor-row">
|
||||
<h5 className="section-heading">Time Options</h5>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label width-7">Timezone</label>
|
||||
<Select isSearchable={true} value={value} onChange={this.onTimeZoneChange} options={timeZones} width={40} />
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-7">Auto-refresh</span>
|
||||
<Input width={60} value={this.getRefreshIntervals()} onChange={this.onRefreshIntervalChange} />
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-7">Now delay now-</span>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
content={'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'}
|
||||
>
|
||||
<Input
|
||||
width={60}
|
||||
invalid={!this.state.isNowDelayValid}
|
||||
placeholder="0m"
|
||||
onChange={this.onNowDelayChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<LegacyForms.Switch
|
||||
labelClass="width-7"
|
||||
label="Hide time picker"
|
||||
checked={dashboard.timepicker.hidden ?? false}
|
||||
onChange={this.onHideTimePickerChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
|
||||
<time-picker-settings getDashboard="ctrl.getDashboard" onTimeZoneChange="ctrl.onTimeZoneChange" onRefreshIntervalChange="ctrl.onRefreshIntervalChange" onNowDelayChange="ctrl.onNowDelayChange" onHideTimePickerChange="ctrl.onHideTimePickerChange"></time-picker-settings>
|
||||
|
||||
<h5 class="section-heading">Panel Options</h5>
|
||||
<div class="gf-form">
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
PanelPlugin,
|
||||
QueryResultMetaStat,
|
||||
SelectableValue,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { config } from 'app/core/config';
|
||||
import { getPanelInspectorStyles } from './styles';
|
||||
@@ -235,6 +236,8 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { dashboard } = this.props;
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: '16px' }}>
|
||||
<div className="section-heading">{name}</div>
|
||||
@@ -244,7 +247,7 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||
return (
|
||||
<tr key={`${stat.title}-${index}`}>
|
||||
<td>{stat.title}</td>
|
||||
<td style={{ textAlign: 'right' }}>{formatStat(stat)}</td>
|
||||
<td style={{ textAlign: 'right' }}>{formatStat(stat, dashboard.getTimezone())}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -331,13 +334,14 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function formatStat(stat: QueryResultMetaStat): string {
|
||||
function formatStat(stat: QueryResultMetaStat, timeZone?: TimeZone): string {
|
||||
const display = getDisplayProcessor({
|
||||
field: {
|
||||
type: FieldType.number,
|
||||
config: stat,
|
||||
},
|
||||
theme: config.theme,
|
||||
timeZone,
|
||||
});
|
||||
return formattedValueToString(display(stat.value));
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('HistoryListCtrl', () => {
|
||||
id: 2,
|
||||
version: 3,
|
||||
formatDate: jest.fn(() => 'date'),
|
||||
getRelativeTime: jest.fn(() => 'time ago'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -148,6 +149,7 @@ describe('HistoryListCtrl', () => {
|
||||
id: 2,
|
||||
version: 3,
|
||||
formatDate: jest.fn(() => 'date'),
|
||||
getRelativeTime: jest.fn(() => 'time ago'),
|
||||
};
|
||||
|
||||
historySrv.calculateDiff = jest.fn(() => Promise.resolve(versionsResponse));
|
||||
|
||||
@@ -3,7 +3,7 @@ import angular, { ILocationService, IScope } from 'angular';
|
||||
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv';
|
||||
import { AppEvents, dateTime, DateTimeInput, locationUtil, toUtc } from '@grafana/data';
|
||||
import { AppEvents, DateTimeInput, locationUtil } from '@grafana/data';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
||||
@@ -76,9 +76,7 @@ export class HistoryListCtrl {
|
||||
}
|
||||
|
||||
formatBasicDate(date: DateTimeInput) {
|
||||
const now = this.dashboard.timezone === 'browser' ? dateTime() : toUtc();
|
||||
const then = this.dashboard.timezone === 'browser' ? dateTime(date) : toUtc(date);
|
||||
return then.from(now);
|
||||
return this.dashboard.getRelativeTime(date);
|
||||
}
|
||||
|
||||
getDiff(diff: 'basic' | 'json') {
|
||||
|
||||
@@ -12,14 +12,13 @@ import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel';
|
||||
import { DashboardMigrator } from './DashboardMigrator';
|
||||
import {
|
||||
AppEvent,
|
||||
dateTime,
|
||||
DateTimeInput,
|
||||
isDateTime,
|
||||
PanelEvents,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
toUtc,
|
||||
UrlQueryValue,
|
||||
dateTimeFormat,
|
||||
dateTimeFormatTimeAgo,
|
||||
} from '@grafana/data';
|
||||
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
|
||||
import { getConfig } from '../../../core/config';
|
||||
@@ -814,11 +813,10 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
formatDate(date: DateTimeInput, format?: string) {
|
||||
date = isDateTime(date) ? date : dateTime(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
const timezone = this.getTimezone();
|
||||
|
||||
return timezone === 'browser' ? dateTime(date).format(format) : toUtc(date).format(format);
|
||||
return dateTimeFormat(date, {
|
||||
format,
|
||||
timeZone: this.getTimezone(),
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -933,13 +931,9 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
getRelativeTime(date: DateTimeInput) {
|
||||
date = isDateTime(date) ? date : dateTime(date);
|
||||
|
||||
return this.timezone === 'browser' ? dateTime(date).fromNow() : toUtc(date).fromNow();
|
||||
}
|
||||
|
||||
isTimezoneUtc() {
|
||||
return this.getTimezone() === 'utc';
|
||||
return dateTimeFormatTimeAgo(date, {
|
||||
timeZone: this.getTimezone(),
|
||||
});
|
||||
}
|
||||
|
||||
isSnapshot() {
|
||||
@@ -947,7 +941,7 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
getTimezone(): TimeZone {
|
||||
return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone;
|
||||
return (this.timezone ? this.timezone : contextSrv?.user?.timezone) as TimeZone;
|
||||
}
|
||||
|
||||
private updateSchema(old: any) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ScopedVars,
|
||||
applyFieldOverrides,
|
||||
DataConfigSource,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface QueryRunnerOptions<
|
||||
@@ -56,6 +57,7 @@ export class PanelQueryRunner {
|
||||
private subscription?: Unsubscribable;
|
||||
private lastResult?: PanelData;
|
||||
private dataConfigSource: DataConfigSource;
|
||||
private timeZone?: TimeZone;
|
||||
|
||||
constructor(dataConfigSource: DataConfigSource) {
|
||||
this.subject = new ReplaySubject(1);
|
||||
@@ -90,6 +92,7 @@ export class PanelQueryRunner {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: applyFieldOverrides({
|
||||
timeZone: this.timeZone,
|
||||
autoMinMax: true,
|
||||
data: processedData.series,
|
||||
...fieldConfig,
|
||||
@@ -118,6 +121,8 @@ export class PanelQueryRunner {
|
||||
minInterval,
|
||||
} = options;
|
||||
|
||||
this.timeZone = timezone;
|
||||
|
||||
if (isSharedDashboardQuery(datasource)) {
|
||||
this.pipeToSubject(runSharedRequest(options));
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTimeForTimeZone } from '@grafana/data';
|
||||
import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTime } from '@grafana/data';
|
||||
|
||||
import {
|
||||
selectThemeVariant,
|
||||
@@ -106,13 +106,14 @@ class UnThemedExploreGraphPanel extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const timeRange = {
|
||||
from: dateTimeForTimeZone(timeZone, absoluteRange.from),
|
||||
to: dateTimeForTimeZone(timeZone, absoluteRange.to),
|
||||
from: dateTime(absoluteRange.from),
|
||||
to: dateTime(absoluteRange.to),
|
||||
raw: {
|
||||
from: dateTimeForTimeZone(timeZone, absoluteRange.from),
|
||||
to: dateTimeForTimeZone(timeZone, absoluteRange.to),
|
||||
from: dateTime(absoluteRange.from),
|
||||
to: dateTime(absoluteRange.to),
|
||||
},
|
||||
};
|
||||
|
||||
const height = showPanel === false ? 100 : showingGraph && showingTable ? 200 : 400;
|
||||
const lineWidth = showLines ? 1 : 5;
|
||||
const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { Themeable, withTheme, getLogRowStyles, Icon } from '@grafana/ui';
|
||||
import { GrafanaTheme, LogRowModel, TimeZone } from '@grafana/data';
|
||||
import { GrafanaTheme, LogRowModel, TimeZone, dateTimeFormat } from '@grafana/data';
|
||||
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
|
||||
@@ -137,7 +137,6 @@ class LiveLogs extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { theme, timeZone, onPause, onResume, isPaused } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
const showUtc = timeZone === 'utc';
|
||||
const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
|
||||
|
||||
return (
|
||||
@@ -151,16 +150,7 @@ class LiveLogs extends PureComponent<Props, State> {
|
||||
{this.rowsToRender().map((row: LogRowModel) => {
|
||||
return (
|
||||
<tr className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
|
||||
{showUtc && (
|
||||
<td className={cx(logsRowLocalTime)} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
||||
{row.timeUtc}
|
||||
</td>
|
||||
)}
|
||||
{!showUtc && (
|
||||
<td className={cx(logsRowLocalTime)} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
||||
{row.timeLocal}
|
||||
</td>
|
||||
)}
|
||||
<td className={cx(logsRowLocalTime)}>{dateTimeFormat(row.timeEpochMs, { timeZone })}</td>
|
||||
<td className={cx(logsRowMessage)}>{row.entry}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -468,8 +468,8 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
};
|
||||
|
||||
const datasourceName = exploreItemState.requestedDatasourceName;
|
||||
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning);
|
||||
const timeZone = getTimeZone(getState().user);
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
|
||||
|
||||
let firstResponse = true;
|
||||
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
const realMomentWrapper = jest.requireActual('@grafana/data/src/datetime/moment_wrapper');
|
||||
|
||||
jest.mock('@grafana/data/src/datetime/moment_wrapper', () => {
|
||||
const momentMock = {
|
||||
dateTime: (ts: any) => {
|
||||
return {
|
||||
valueOf: () => ts,
|
||||
fromNow: () => 'fromNow() jest mocked',
|
||||
format: (fmt: string) => 'format() jest mocked',
|
||||
};
|
||||
},
|
||||
toUtc: null as any,
|
||||
isDateTime: realMomentWrapper.isDateTime,
|
||||
};
|
||||
momentMock.toUtc = (ts: any) => ({
|
||||
format: (fmt: string) => 'format() jest mocked',
|
||||
local: () => momentMock.dateTime(ts),
|
||||
});
|
||||
|
||||
return momentMock;
|
||||
});
|
||||
jest.mock('@grafana/data/src/datetime/formatter', () => ({
|
||||
dateTimeFormat: () => 'format() jest mocked',
|
||||
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
|
||||
}));
|
||||
|
||||
import { ResultProcessor } from './ResultProcessor';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
|
||||
@@ -92,6 +92,7 @@ export class ResultProcessor {
|
||||
field.display = getDisplayProcessor({
|
||||
field,
|
||||
theme: config.theme,
|
||||
timeZone: this.timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { Forms, Form } from '@grafana/ui';
|
||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { ImportDashboardForm } from './ImportDashboardForm';
|
||||
@@ -73,7 +73,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Updated on</td>
|
||||
<td>{dateTime(meta.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||
<td>{dateTimeFormat(meta.updatedAt)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -185,7 +185,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
queries: panel.targets,
|
||||
panelId: panel.id,
|
||||
dashboardId: this.dashboard.id,
|
||||
timezone: this.dashboard.timezone,
|
||||
timezone: this.dashboard.getTimezone(),
|
||||
timeInfo: this.timeInfo,
|
||||
timeRange: this.range,
|
||||
widthPixels: this.width,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { coreModule, NavModelSrv } from 'app/core/core';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||
import { UserSession } from 'app/types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||
@@ -36,8 +36,8 @@ export class ProfileCtrl {
|
||||
return {
|
||||
id: session.id,
|
||||
isActive: session.isActive,
|
||||
seenAt: dateTime(session.seenAt).fromNow(),
|
||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
||||
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||
clientIp: session.clientIp,
|
||||
browser: session.browser,
|
||||
browserVersion: session.browserVersion,
|
||||
|
||||
@@ -46,7 +46,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
};
|
||||
|
||||
getValues = (): FieldDisplay[] => {
|
||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
||||
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||
return getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
reduceOptions: options.reduceOptions,
|
||||
@@ -54,6 +54,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
theme: config.theme,
|
||||
data: data.series,
|
||||
autoMinMax: true,
|
||||
timeZone,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
};
|
||||
|
||||
getValues = (): FieldDisplay[] => {
|
||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
||||
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||
return getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
reduceOptions: options.reduceOptions,
|
||||
@@ -47,6 +47,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
theme: config.theme,
|
||||
data: data.series,
|
||||
autoMinMax: true,
|
||||
timeZone,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import ReactDOM from 'react-dom';
|
||||
import { GraphLegendProps, Legend } from './Legend/Legend';
|
||||
|
||||
import { GraphCtrl } from './module';
|
||||
import { ContextMenuGroup, ContextMenuItem } from '@grafana/ui';
|
||||
import { ContextMenuGroup, ContextMenuItem, graphTimeFormatter, graphTimeFormat } from '@grafana/ui';
|
||||
import { provideTheme, getCurrentTheme } from 'app/core/utils/ConfigProvider';
|
||||
import {
|
||||
toUtc,
|
||||
@@ -270,6 +270,7 @@ class GraphElement {
|
||||
const fieldDisplay = getDisplayProcessor({
|
||||
field: { config: fieldConfig, type: FieldType.number },
|
||||
theme: getCurrentTheme(),
|
||||
timeZone: this.dashboard.getTimezone(),
|
||||
})(field.values.get(dataIndex));
|
||||
linksSupplier = links.length
|
||||
? getFieldLinksSupplier({
|
||||
@@ -643,7 +644,8 @@ class GraphElement {
|
||||
max: max,
|
||||
label: 'Datetime',
|
||||
ticks: ticks,
|
||||
timeformat: this.time_format(ticks, min, max),
|
||||
timeformat: graphTimeFormat(ticks, min, max),
|
||||
timeFormatter: graphTimeFormatter(this.dashboard.getTimezone()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -900,33 +902,6 @@ class GraphElement {
|
||||
return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals));
|
||||
};
|
||||
}
|
||||
|
||||
time_format(ticks: number, min: number | null, max: number | null) {
|
||||
if (min && max && ticks) {
|
||||
const range = max - min;
|
||||
const secPerTick = range / ticks / 1000;
|
||||
// Need have 10 millisecond margin on the day range
|
||||
// As sometimes last 24 hour dashboard evaluates to more than 86400000
|
||||
const oneDay = 86400010;
|
||||
const oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return '%H:%M:%S';
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return '%H:%M';
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return '%m/%d %H:%M';
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return '%m/%d';
|
||||
}
|
||||
return '%Y-%m';
|
||||
}
|
||||
|
||||
return '%H:%M';
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
|
||||
@@ -12,14 +12,7 @@ import { axesEditorComponent } from './axes_editor';
|
||||
import config from 'app/core/config';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
||||
import {
|
||||
getColorFromHexRgbOrName,
|
||||
PanelEvents,
|
||||
DataFrame,
|
||||
DataLink,
|
||||
DateTimeInput,
|
||||
VariableSuggestion,
|
||||
} from '@grafana/data';
|
||||
import { getColorFromHexRgbOrName, PanelEvents, DataFrame, DataLink, VariableSuggestion } from '@grafana/data';
|
||||
|
||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
@@ -344,9 +337,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
this.contextMenuCtrl.toggleMenu();
|
||||
};
|
||||
|
||||
formatDate = (date: DateTimeInput, format?: string) => {
|
||||
return this.dashboard.formatDate.apply(this.dashboard, [date, format]);
|
||||
};
|
||||
getTimeZone = () => this.dashboard.getTimezone();
|
||||
|
||||
getDataFrameByRefId = (refId: string) => {
|
||||
return this.dataList.filter(dataFrame => dataFrame.refId === refId)[0];
|
||||
|
||||
@@ -517,7 +517,7 @@ describe('grafanaGraph', () => {
|
||||
|
||||
it('should format dates as hours minutes', () => {
|
||||
const axis = ctx.plotOptions.xaxis;
|
||||
expect(axis.timeformat).toBe('%H:%M');
|
||||
expect(axis.timeformat).toBe('HH:mm');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -531,7 +531,7 @@ describe('grafanaGraph', () => {
|
||||
|
||||
it('should format dates as month days', () => {
|
||||
const axis = ctx.plotOptions.xaxis;
|
||||
expect(axis.timeformat).toBe('%m/%d');
|
||||
expect(axis.timeformat).toBe('MM/DD');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ const template = `
|
||||
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
|
||||
onClose="ctrl.onContextMenuClose"
|
||||
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
||||
formatSourceDate="ctrl.formatDate"
|
||||
timeZone="ctrl.getTimeZone()"
|
||||
x="ctrl.contextMenuCtrl.position.x"
|
||||
y="ctrl.contextMenuCtrl.position.y"
|
||||
></graph-context-menu>
|
||||
|
||||
@@ -38,6 +38,7 @@ export const getGraphSeriesModel = (
|
||||
decimals: legendOptions.decimals,
|
||||
},
|
||||
},
|
||||
timeZone,
|
||||
});
|
||||
|
||||
let fieldColumnIndex = -1;
|
||||
@@ -103,7 +104,7 @@ export const getGraphSeriesModel = (
|
||||
}
|
||||
: { ...field.config, color };
|
||||
|
||||
field.display = getDisplayProcessor({ field });
|
||||
field.display = getDisplayProcessor({ field, timeZone });
|
||||
|
||||
// Time step is used to determine bars width when graph is rendered as bar chart
|
||||
const timeStep = getSeriesTimeStep(timeField);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getColorFromHexRgbOrName,
|
||||
getValueFormat,
|
||||
formattedValueToString,
|
||||
dateTimeFormat,
|
||||
} from '@grafana/data';
|
||||
import { CoreEvents } from 'app/types';
|
||||
|
||||
@@ -154,19 +155,14 @@ export class HeatmapRenderer {
|
||||
.range([0, this.chartWidth]);
|
||||
|
||||
const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
||||
const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
|
||||
let timeFormat;
|
||||
const dashboardTimeZone = this.ctrl.dashboard.getTimezone();
|
||||
if (dashboardTimeZone === 'utc') {
|
||||
timeFormat = d3.utcFormat(grafanaTimeFormatter);
|
||||
} else {
|
||||
timeFormat = d3.timeFormat(grafanaTimeFormatter);
|
||||
}
|
||||
const format = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
|
||||
const timeZone = this.ctrl.dashboard.getTimezone();
|
||||
const formatter = (date: Date) => dateTimeFormat(date, { format, timeZone });
|
||||
|
||||
const xAxis = d3
|
||||
.axisBottom(this.xScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(timeFormat)
|
||||
.tickFormat(formatter)
|
||||
.tickPadding(X_AXIS_TICK_PADDING)
|
||||
.tickSize(this.chartHeight);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { feedToDataFrame } from './utils';
|
||||
import { loadRSSFeed } from './rss';
|
||||
|
||||
// Types
|
||||
import { PanelProps, DataFrameView, dateTime, GrafanaTheme, textUtil } from '@grafana/data';
|
||||
import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme, textUtil } from '@grafana/data';
|
||||
import { NewsOptions, NewsItem } from './types';
|
||||
import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants';
|
||||
import { css } from 'emotion';
|
||||
@@ -79,7 +79,7 @@ export class NewsPanel extends PureComponent<Props, State> {
|
||||
<div key={index} className={styles.item}>
|
||||
<a href={item.link} target="_blank">
|
||||
<div className={styles.title}>{item.title}</div>
|
||||
<div className={styles.date}>{dateTime(item.date).format('MMM DD')} </div>
|
||||
<div className={styles.date}>{dateTimeFormat(item.date, { format: 'MMM DD' })} </div>
|
||||
</a>
|
||||
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Props extends PanelProps<PieChartOptions> {}
|
||||
|
||||
export class PieChartPanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { width, height, options, data, replaceVariables, fieldConfig } = this.props;
|
||||
const { width, height, options, data, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||
|
||||
const values = getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
@@ -24,6 +24,7 @@ export class PieChartPanel extends PureComponent<Props> {
|
||||
data: data.series,
|
||||
theme: config.theme,
|
||||
replaceVariables: replaceVariables,
|
||||
timeZone,
|
||||
}).map(v => v.display);
|
||||
|
||||
return (
|
||||
|
||||
@@ -187,6 +187,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
},
|
||||
},
|
||||
theme: config.theme,
|
||||
timeZone: this.dashboard.getTimezone(),
|
||||
});
|
||||
// When we don't have any field
|
||||
this.data = {
|
||||
|
||||
@@ -69,7 +69,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
||||
};
|
||||
|
||||
getValues = (): FieldDisplay[] => {
|
||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
||||
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||
|
||||
return getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
@@ -79,6 +79,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
||||
data: data.series,
|
||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||
autoMinMax: true,
|
||||
timeZone,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
this.renderer = new TableRenderer(
|
||||
this.panel,
|
||||
this.table,
|
||||
this.dashboard.isTimezoneUtc(),
|
||||
this.dashboard.getTimezone(),
|
||||
this.$sanitize,
|
||||
this.templateSrv,
|
||||
config.theme.type
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
dateTime,
|
||||
escapeStringForRegex,
|
||||
formattedValueToString,
|
||||
getColorFromHexRgbOrName,
|
||||
@@ -11,6 +10,9 @@ import {
|
||||
stringToJsRegex,
|
||||
textUtil,
|
||||
unEscapeStringFromRegex,
|
||||
TimeZone,
|
||||
dateTimeFormatISO,
|
||||
dateTimeFormat,
|
||||
} from '@grafana/data';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
|
||||
@@ -23,7 +25,7 @@ export class TableRenderer {
|
||||
constructor(
|
||||
private panel: { styles: ColumnStyle[]; pageSize: number },
|
||||
private table: TableRenderModel,
|
||||
private isUtc: boolean,
|
||||
private timeZone: TimeZone,
|
||||
private sanitize: (v: any) => any,
|
||||
private templateSrv: TemplateSrv,
|
||||
private theme?: GrafanaThemeType
|
||||
@@ -119,13 +121,16 @@ export class TableRenderer {
|
||||
v = parseInt(v, 10);
|
||||
}
|
||||
|
||||
let date = dateTime(v);
|
||||
|
||||
if (this.isUtc) {
|
||||
date = date.utc();
|
||||
if (!column.style.dateFormat) {
|
||||
return dateTimeFormatISO(v, {
|
||||
timeZone: this.timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
return date.format(column.style.dateFormat);
|
||||
return dateTimeFormat(v, {
|
||||
format: column.style.dateFormat,
|
||||
timeZone: this.timeZone,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { TableRenderer } from '../renderer';
|
||||
import { getColorDefinitionByName, ScopedVars } from '@grafana/data';
|
||||
import { getColorDefinitionByName, ScopedVars, TimeZone } from '@grafana/data';
|
||||
import { ColumnRender } from '../types';
|
||||
|
||||
const utc: TimeZone = 'utc';
|
||||
|
||||
const sanitize = (value: any): string => {
|
||||
return 'sanitized';
|
||||
};
|
||||
@@ -210,7 +212,7 @@ describe('when rendering table', () => {
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
|
||||
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv);
|
||||
|
||||
it('time column should be formatted', () => {
|
||||
const html = renderer.renderCell(0, 0, 1388556366666);
|
||||
@@ -466,7 +468,7 @@ describe('when rendering table with different patterns', () => {
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
|
||||
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv);
|
||||
const html = renderer.renderCell(1, 0, 1230);
|
||||
|
||||
expect(html).toBe(expected);
|
||||
@@ -536,7 +538,7 @@ describe('when rendering cells with different alignment options', () => {
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
|
||||
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv);
|
||||
const html = renderer.renderCell(1, 0, 42);
|
||||
|
||||
expect(html).toBe(expected);
|
||||
|
||||
14
public/vendor/flot/jquery.flot.time.js
vendored
14
public/vendor/flot/jquery.flot.time.js
vendored
@@ -15,7 +15,8 @@ API.txt for details.
|
||||
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||
timeformat: null, // format string to use
|
||||
twelveHourClock: false, // 12 or 24 time in time mode
|
||||
monthNames: null // list of names of months
|
||||
monthNames: null, // list of names of months
|
||||
timeFormatter: null // external formatter with timezone support
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +30,6 @@ API.txt for details.
|
||||
// A subset of the Open Group's strftime format is supported.
|
||||
|
||||
function formatDate(d, fmt, monthNames, dayNames) {
|
||||
|
||||
if (typeof d.strftime == "function") {
|
||||
return d.strftime(fmt);
|
||||
}
|
||||
@@ -356,12 +356,15 @@ API.txt for details.
|
||||
};
|
||||
|
||||
axis.tickFormatter = function (v, axis) {
|
||||
|
||||
var d = dateGenerator(v, axis.options);
|
||||
|
||||
// first check global format
|
||||
// first check global formatter
|
||||
if (typeof opts.timeFormatter === "function") {
|
||||
return opts.timeFormatter(d.getTime(), opts.timeformat);
|
||||
}
|
||||
|
||||
if (opts.timeformat != null) {
|
||||
// second check global format
|
||||
if (opts.timeformat != null) {
|
||||
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
|
||||
}
|
||||
|
||||
@@ -407,7 +410,6 @@ API.txt for details.
|
||||
}
|
||||
|
||||
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
|
||||
|
||||
return rt;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user