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:
Marcus Andersson
2020-06-26 09:08:15 +02:00
committed by GitHub
parent 084542a006
commit 1abbb477cf
38 changed files with 1559 additions and 898 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -63,6 +63,7 @@ function createToolbar(supportedModes: ExploreMode[]) {
setDashboardQueriesToUpdateOnLoad={(() => {}) as any}
exploreId={ExploreId.left}
onChangeTime={(() => {}) as any}
onChangeTimeZone={(() => {}) as any}
/>
);
}

View File

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

View File

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