mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeZone: unify the time zone pickers to one that can rule them all. (#24803)
* draft on a unified TimeZonePicker. * most of the data structures is in place. * wip. * wip. * wip: timezone selector in progress.2 * fixed so we have proper data on all timezones. * started to add timezone into time picker. * addeing time zone footer. * footer is working. * fixed so we use the timeZone picker in shared preferences. * Added so we can change timeZone from picker. * did some styling changes. * will update timezone on all places that we need to update it. * removed console.log * removed magic string. * fixed border on calendar. * ignoring eslint cache. * cleaned up the code a bit. * made the default selectable. * corrected so the behaviour about default works as expected. * excluded timezone from change tracker. * revert so default will always be the intial value. * default will always fallback to the one in the config. * do the country mapping on startup. * fixed nit. * updated snapshots for timepicker. * fixed build errors. * updating so snapshot tests is in sync. * removed Date.now from prop since it will change each run in the snapshot tests. * fixed so e2e tests works as before. * moved files into separate folders.
This commit is contained in:
@@ -94,7 +94,7 @@ export class GrafanaApp {
|
||||
|
||||
addClassIfNoOverlayScrollbar();
|
||||
setLocale(config.bootData.user.locale);
|
||||
setTimeZoneResolver(() => config.bootData.user.timeZone);
|
||||
setTimeZoneResolver(() => config.bootData.user.timezone);
|
||||
|
||||
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
Button,
|
||||
RadioButtonGroup,
|
||||
FieldSet,
|
||||
TimeZonePicker,
|
||||
} from '@grafana/ui';
|
||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
@@ -36,18 +37,6 @@ const themes: SelectableValue[] = [
|
||||
{ value: 'light', label: 'Light' },
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
@@ -112,11 +101,11 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
this.setState({ theme: value });
|
||||
};
|
||||
|
||||
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
||||
if (!timezone || typeof timezone.value !== 'string') {
|
||||
onTimeZoneChanged = (timezone: string) => {
|
||||
if (!timezone) {
|
||||
return;
|
||||
}
|
||||
this.setState({ timezone: timezone.value });
|
||||
this.setState({ timezone: timezone });
|
||||
};
|
||||
|
||||
onHomeDashboardChanged = (dashboardId: number) => {
|
||||
@@ -168,12 +157,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
</Field>
|
||||
|
||||
<Field label="Timezone" aria-label={selectors.components.TimeZonePicker.container}>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
value={timeZones.find(item => item.value === timezone)}
|
||||
onChange={this.onTimeZoneChanged}
|
||||
options={timeZones}
|
||||
/>
|
||||
<TimeZonePicker value={timezone} onChange={this.onTimeZoneChanged} />
|
||||
</Field>
|
||||
<div className="gf-form-button-row">
|
||||
<Button variant="primary">Save</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Libaries
|
||||
import React, { PureComponent, FC, ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, MapDispatchToProps } from 'react-redux';
|
||||
import { css } from 'emotion';
|
||||
// Utils & Services
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
@@ -13,6 +13,7 @@ import { textUtil } from '@grafana/data';
|
||||
import { BackButton } from 'app/core/components/BackButton/BackButton';
|
||||
// State
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
// Types
|
||||
import { DashboardModel } from '../../state';
|
||||
import { CoreEvents, StoreState } from 'app/types';
|
||||
@@ -23,10 +24,14 @@ export interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
isFullscreen: boolean;
|
||||
$injector: any;
|
||||
updateLocation: typeof updateLocation;
|
||||
onAddPanel: () => void;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
updateTimeZoneForSession: typeof updateTimeZoneForSession;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
interface DashNavButtonModel {
|
||||
show: (props: Props) => boolean;
|
||||
component: FC<Partial<Props>>;
|
||||
@@ -48,7 +53,7 @@ export interface StateProps {
|
||||
location: any;
|
||||
}
|
||||
|
||||
type Props = StateProps & OwnProps;
|
||||
type Props = StateProps & OwnProps & DispatchProps;
|
||||
|
||||
class DashNav extends PureComponent<Props> {
|
||||
playlistSrv: PlaylistSrv;
|
||||
@@ -277,7 +282,7 @@ class DashNav extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, location, isFullscreen } = this.props;
|
||||
const { dashboard, location, isFullscreen, updateTimeZoneForSession } = this.props;
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
@@ -315,7 +320,11 @@ class DashNav extends PureComponent<Props> {
|
||||
|
||||
{!dashboard.timepicker.hidden && (
|
||||
<div className="navbar-buttons">
|
||||
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
|
||||
<DashNavTimeControls
|
||||
dashboard={dashboard}
|
||||
location={location}
|
||||
onChangeTimeZone={updateTimeZoneForSession}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -327,8 +336,9 @@ const mapStateToProps = (state: StoreState) => ({
|
||||
location: state.location,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
updateLocation,
|
||||
updateTimeZoneForSession,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DashNav);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Libaries
|
||||
import React, { Component } from 'react';
|
||||
import { dateMath, GrafanaTheme } from '@grafana/data';
|
||||
import { dateMath, GrafanaTheme, TimeZone } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
|
||||
// Types
|
||||
@@ -9,7 +9,7 @@ import { LocationState, CoreEvents } from 'app/types';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
// State
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
|
||||
// Components
|
||||
import { RefreshPicker, withTheme, stylesFactory, Themeable } from '@grafana/ui';
|
||||
@@ -31,8 +31,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
|
||||
export interface Props extends Themeable {
|
||||
dashboard: DashboardModel;
|
||||
updateLocation: typeof updateLocation;
|
||||
location: LocationState;
|
||||
onChangeTimeZone: typeof updateTimeZoneForSession;
|
||||
}
|
||||
class UnthemedDashNavTimeControls extends Component<Props> {
|
||||
componentDidMount() {
|
||||
@@ -87,6 +87,12 @@ class UnthemedDashNavTimeControls extends Component<Props> {
|
||||
getTimeSrv().setTime(nextRange);
|
||||
};
|
||||
|
||||
onChangeTimeZone = (timeZone: TimeZone) => {
|
||||
this.props.dashboard.timezone = timeZone;
|
||||
this.props.onChangeTimeZone(timeZone);
|
||||
this.onRefresh();
|
||||
};
|
||||
|
||||
onZoom = () => {
|
||||
appEvents.emit(CoreEvents.zoomOut, 2);
|
||||
};
|
||||
@@ -109,6 +115,7 @@ class UnthemedDashNavTimeControls extends Component<Props> {
|
||||
onMoveBackward={this.onMoveBack}
|
||||
onMoveForward={this.onMoveForward}
|
||||
onZoom={this.onZoom}
|
||||
onChangeTimeZone={this.onChangeTimeZone}
|
||||
/>
|
||||
<RefreshPicker
|
||||
onIntervalChanged={this.onChangeRefreshInterval}
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Select, Input, Tooltip, LegacyForms } from '@grafana/ui';
|
||||
import { TimeZonePicker, Input, Tooltip, LegacyForms } from '@grafana/ui';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { getTimeZoneGroups, TimeZone, rangeUtil, SelectableValue } from '@grafana/data';
|
||||
import { TimeZone, rangeUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
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;
|
||||
@@ -97,17 +85,16 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onTimeZoneChange = (timeZone: SelectableValue<string>) => {
|
||||
if (!timeZone || typeof timeZone.value !== 'string') {
|
||||
onTimeZoneChange = (timeZone: string) => {
|
||||
if (typeof timeZone !== 'string') {
|
||||
return;
|
||||
}
|
||||
this.props.onTimeZoneChange(timeZone.value);
|
||||
this.props.onTimeZoneChange(timeZone);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
const dashboard = this.props.getDashboard();
|
||||
const value = timeZones.find(item => item.value === dashboard.timezone);
|
||||
|
||||
return (
|
||||
<div className="editor-row">
|
||||
@@ -115,7 +102,7 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form" aria-label={selectors.components.TimeZonePicker.container}>
|
||||
<label className="gf-form-label width-7">Timezone</label>
|
||||
<Select isSearchable={true} value={value} onChange={this.onTimeZoneChange} options={timeZones} width={40} />
|
||||
<TimeZonePicker value={dashboard.timezone} onChange={this.onTimeZoneChange} width={40} />
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
|
||||
@@ -24,6 +24,7 @@ import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
|
||||
import { getPanelEditorTabs } from './state/selectors';
|
||||
import { getPanelStateById } from '../../state/selectors';
|
||||
import { OptionsPaneContent } from './OptionsPaneContent';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { getVariables } from 'app/features/variables/state/selectors';
|
||||
@@ -54,6 +55,7 @@ interface DispatchProps {
|
||||
panelEditorCleanUp: typeof panelEditorCleanUp;
|
||||
setDiscardChanges: typeof setDiscardChanges;
|
||||
updatePanelEditorUIState: typeof updatePanelEditorUIState;
|
||||
updateTimeZoneForSession: typeof updateTimeZoneForSession;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
@@ -220,7 +222,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
renderPanelToolbar(styles: EditorStyles) {
|
||||
const { dashboard, location, uiState, variables } = this.props;
|
||||
const { dashboard, location, uiState, variables, updateTimeZoneForSession } = this.props;
|
||||
return (
|
||||
<div className={styles.panelToolbar}>
|
||||
<HorizontalGroup justify={variables.length > 0 ? 'space-between' : 'flex-end'} align="flex-start">
|
||||
@@ -228,7 +230,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
|
||||
<HorizontalGroup>
|
||||
<RadioButtonGroup value={uiState.mode} options={displayModes} onChange={this.onDiplayModeChange} />
|
||||
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
|
||||
<DashNavTimeControls
|
||||
dashboard={dashboard}
|
||||
location={location}
|
||||
onChangeTimeZone={updateTimeZoneForSession}
|
||||
/>
|
||||
{!uiState.isPanelOptionsVisible && (
|
||||
<DashNavButton
|
||||
onClick={this.onTogglePanelOptions}
|
||||
@@ -362,6 +368,7 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
panelEditorCleanUp,
|
||||
setDiscardChanges,
|
||||
updatePanelEditorUIState,
|
||||
updateTimeZoneForSession,
|
||||
};
|
||||
|
||||
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected);
|
||||
|
||||
@@ -112,6 +112,7 @@ export class ChangeTracker {
|
||||
dash.time = 0;
|
||||
dash.refresh = 0;
|
||||
dash.schemaVersion = 0;
|
||||
dash.timezone = 0;
|
||||
|
||||
// ignore iteration property
|
||||
delete dash.iteration;
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface Props {
|
||||
syncedTimes: boolean;
|
||||
onChangeTimeSync: () => void;
|
||||
onChangeTime: (range: RawTimeRange) => void;
|
||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||
}
|
||||
|
||||
export class ExploreTimeControls extends Component<Props> {
|
||||
@@ -56,7 +57,7 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText } = this.props;
|
||||
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText, onChangeTimeZone } = this.props;
|
||||
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
|
||||
const timePickerCommonProps = {
|
||||
value: range,
|
||||
@@ -73,6 +74,7 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
timeSyncButton={timeSyncButton}
|
||||
isSynced={syncedTimes}
|
||||
onChange={this.onChangeTimePicker}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ function createToolbar(supportedModes: ExploreMode[]) {
|
||||
setDashboardQueriesToUpdateOnLoad={(() => {}) as any}
|
||||
exploreId={ExploreId.left}
|
||||
onChangeTime={(() => {}) as any}
|
||||
onChangeTimeZone={(() => {}) as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from './state/actions';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
import { updateTimeZoneForSession } from '../profile/state/reducers';
|
||||
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||
import kbn from '../../core/utils/kbn';
|
||||
import { ExploreTimeControls } from './ExploreTimeControls';
|
||||
@@ -83,6 +84,7 @@ interface DispatchProps {
|
||||
changeMode: typeof changeMode;
|
||||
updateLocation: typeof updateLocation;
|
||||
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
|
||||
onChangeTimeZone: typeof updateTimeZoneForSession;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & OwnProps;
|
||||
@@ -180,6 +182,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
originPanelId,
|
||||
datasourceLoading,
|
||||
containerWidth,
|
||||
onChangeTimeZone,
|
||||
} = this.props;
|
||||
|
||||
const styles = getStyles();
|
||||
@@ -303,6 +306,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
syncedTimes={syncedTimes}
|
||||
onChangeTimeSync={this.onChangeTimeSync}
|
||||
hideText={showSmallTimePicker}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -410,6 +414,7 @@ const mapDispatchToProps: DispatchProps = {
|
||||
syncTimes,
|
||||
changeMode: changeMode,
|
||||
setDashboardQueriesToUpdateOnLoad,
|
||||
onChangeTimeZone: updateTimeZoneForSession,
|
||||
};
|
||||
|
||||
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
import { UserState } from 'app/types';
|
||||
import _ from 'lodash';
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { UserState, ThunkResult } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
import { TimeZone } from '@grafana/data';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
export const initialState: UserState = {
|
||||
orgId: config.bootData.user.orgId,
|
||||
timeZone: config.bootData.user.timezone,
|
||||
};
|
||||
|
||||
export const userReducer = (state = initialState, action: any): UserState => {
|
||||
return state;
|
||||
export const slice = createSlice({
|
||||
name: 'user/profile',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateTimeZone: (state, action: PayloadAction<TimeZone>): UserState => {
|
||||
return {
|
||||
...state,
|
||||
timeZone: action.payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const { updateTimeZone } = slice.actions;
|
||||
|
||||
if (!_.isString(timeZone) || _.isEmpty(timeZone)) {
|
||||
timeZone = config?.bootData?.user?.timezone;
|
||||
}
|
||||
|
||||
_.set(contextSrv, 'user.timezone', timeZone);
|
||||
dispatch(updateTimeZone(timeZone));
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
user: userReducer,
|
||||
};
|
||||
export const userReducer = slice.reducer;
|
||||
export default { user: slice.reducer };
|
||||
|
||||
Reference in New Issue
Block a user