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:
Marcus Andersson
2020-04-27 15:28:06 +02:00
committed by GitHub
parent 365de313f3
commit 1a0c1a39e4
77 changed files with 821 additions and 576 deletions

View File

@@ -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 });

View File

@@ -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 }],
]);
}

View File

@@ -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>

View File

@@ -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,
};
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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';
}
/**

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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 (
<>

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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>
);
}
}

View File

@@ -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">

View File

@@ -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));
}

View File

@@ -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));

View File

@@ -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') {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -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 }));

View File

@@ -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';

View File

@@ -92,6 +92,7 @@ export class ResultProcessor {
field.display = getDisplayProcessor({
field,
theme: config.theme,
timeZone: this.timeZone,
});
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
});
};

View File

@@ -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,
});
};

View File

@@ -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 */

View File

@@ -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];

View File

@@ -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');
});
});
});

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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 (

View File

@@ -187,6 +187,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
},
},
theme: config.theme,
timeZone: this.dashboard.getTimezone(),
});
// When we don't have any field
this.data = {

View File

@@ -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,
});
};

View File

@@ -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

View File

@@ -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,
});
};
}

View File

@@ -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);

View File

@@ -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;
};
}