diff --git a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx index 8dd915b39f7..377f41b38fa 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx @@ -1,5 +1,8 @@ // Libraries import React, { PureComponent, createRef } from 'react'; +import { css } from 'emotion'; +import memoizeOne from 'memoize-one'; +import classNames from 'classnames'; // Components import { ButtonSelect } from '../Select/ButtonSelect'; @@ -11,15 +14,41 @@ import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper' import { isDateTime, DateTime } from '@grafana/data'; import { rangeUtil } from '@grafana/data'; import { rawToTimeRange } from './time'; +import { withTheme } from '../../themes/ThemeContext'; // Types -import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue } from '@grafana/data'; -import { dateMath } from '@grafana/data'; +import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue, dateMath } from '@grafana/data'; +import { GrafanaTheme } from '../../types/theme'; +import { Themeable } from '../../types'; -export interface Props { +const getStyles = memoizeOne((theme: GrafanaTheme) => { + return { + timePickerSynced: css` + label: timePickerSynced; + border-color: ${theme.colors.orangeDark}; + background-image: none; + background-color: transparent; + color: ${theme.colors.orangeDark}; + &:focus, + :hover { + color: ${theme.colors.orangeDark}; + background-image: none; + background-color: transparent; + } + `, + noRightBorderStyle: css` + label: noRightBorderStyle; + border-right: 0; + `, + }; +}); + +export interface Props extends Themeable { value: TimeRange; selectOptions: TimeOption[]; timeZone?: TimeZone; + timeSyncButton?: JSX.Element; + isSynced?: boolean; onChange: (timeRange: TimeRange) => void; onMoveBackward: () => void; onMoveForward: () => void; @@ -70,7 +99,7 @@ const defaultZoomOutTooltip = () => { export interface State { isCustomOpen: boolean; } -export class TimePicker extends PureComponent { +class UnThemedTimePicker extends PureComponent { pickerTriggerRef = createRef(); state: State = { @@ -120,7 +149,19 @@ export class TimePicker extends PureComponent { }; render() { - const { selectOptions: selectTimeOptions, value, onMoveBackward, onMoveForward, onZoom, timeZone } = this.props; + const { + selectOptions: selectTimeOptions, + value, + onMoveBackward, + onMoveForward, + onZoom, + timeZone, + timeSyncButton, + isSynced, + theme, + } = this.props; + + const styles = getStyles(theme); const { isCustomOpen } = this.state; const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions); const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value)); @@ -152,7 +193,10 @@ export class TimePicker extends PureComponent { )} { iconClass={'fa fa-clock-o fa-fw'} tooltipContent={} /> + + {timeSyncButton} + {isAbsolute && ( + + ); +} diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 03ce5883906..1d821be3e82 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -13,6 +13,7 @@ import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFact export enum ActionTypes { SplitOpen = 'explore/SPLIT_OPEN', ResetExplore = 'explore/RESET_EXPLORE', + SyncTimes = 'explore/SYNC_TIMES', } export interface SplitOpenAction { type: ActionTypes.SplitOpen; @@ -26,6 +27,10 @@ export interface ResetExploreAction { payload: {}; } +export interface SyncTimesAction { + type: ActionTypes.SyncTimes; + payload: { syncedTimes: boolean }; +} /** Lower order actions * */ @@ -165,6 +170,10 @@ export interface SplitOpenPayload { itemState: ExploreItemState; } +export interface SyncTimesPayload { + syncedTimes: boolean; +} + export interface ToggleTablePayload { exploreId: ExploreId; } @@ -352,6 +361,7 @@ export const splitCloseAction = actionCreatorFactory('e */ export const splitOpenAction = actionCreatorFactory('explore/SPLIT_OPEN').create(); +export const syncTimesAction = actionCreatorFactory('explore/SYNC_TIMES').create(); /** * Update state of Explores UI elements (panels visiblity and deduplication strategy) */ @@ -410,4 +420,5 @@ export type HigherOrderAction = | ActionOf | SplitOpenAction | ResetExploreAction + | SyncTimesAction | ActionOf; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 02d953b7dde..951a2512b4d 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -71,6 +71,7 @@ import { queryStreamUpdatedAction, queryStoreSubscriptionAction, clearOriginAction, + syncTimesAction, } from './actionTypes'; import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; import { getTimeZone } from 'app/features/profile/state/selectors'; @@ -182,12 +183,19 @@ export const updateTimeRange = (options: { rawRange?: RawTimeRange; absoluteRange?: AbsoluteTimeRange; }): ThunkResult => { - return dispatch => { - dispatch(updateTime({ ...options })); - dispatch(runQueries(options.exploreId)); + return (dispatch, getState) => { + const { syncedTimes } = getState().explore; + if (syncedTimes) { + dispatch(updateTime({ ...options, exploreId: ExploreId.left })); + dispatch(runQueries(ExploreId.left)); + dispatch(updateTime({ ...options, exploreId: ExploreId.right })); + dispatch(runQueries(ExploreId.right)); + } else { + dispatch(updateTime({ ...options })); + dispatch(runQueries(options.exploreId)); + } }; }; - /** * Change the refresh interval of Explore. Called from the Refresh picker. */ @@ -674,6 +682,25 @@ export function splitOpen(): ThunkResult { }; } +/** + * Syncs time interval, if they are not synced on both panels in a split mode. + * Unsyncs time interval, if they are synced on both panels in a split mode. + */ +export function syncTimes(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + if (exploreId === ExploreId.left) { + const leftState = getState().explore.left; + dispatch(updateTimeRange({ exploreId: ExploreId.right, rawRange: leftState.range.raw })); + } else { + const rightState = getState().explore.right; + dispatch(updateTimeRange({ exploreId: ExploreId.left, rawRange: rightState.range.raw })); + } + const isTimeSynced = getState().explore.syncedTimes; + dispatch(syncTimesAction({ syncedTimes: !isTimeSynced })); + dispatch(stateSave()); + }; +} + /** * Creates action to collapse graph/logs/table panel. When panel is collapsed, * queries won't be run diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index f19760a53d5..b6dd7bc82fc 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -129,6 +129,7 @@ export const createEmptyQueryResponse = (): PanelData => ({ export const initialExploreItemState = makeExploreItemState(); export const initialExploreState: ExploreState = { split: null, + syncedTimes: false, left: initialExploreItemState, right: initialExploreItemState, }; @@ -727,6 +728,9 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA case ActionTypes.SplitOpen: { return { ...state, split: true, right: { ...action.payload.itemState } }; } + case ActionTypes.SyncTimes: { + return { ...state, syncedTimes: action.payload.syncedTimes }; + } case ActionTypes.ResetExplore: { if (action.payload.force || !Number.isInteger(state.left.originPanelId)) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 90c691839ff..e8690c507e6 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -130,6 +130,10 @@ export interface ExploreState { * True if split view is active. */ split: boolean; + /** + * True if time interval for panels are synced. Only possible with split mode. + */ + syncedTimes: boolean; /** * Explore state of the left split (left is default in non-split view). */ diff --git a/public/test/mocks/mockExploreState.ts b/public/test/mocks/mockExploreState.ts index 108b4a63faf..e0b6ba9c3ed 100644 --- a/public/test/mocks/mockExploreState.ts +++ b/public/test/mocks/mockExploreState.ts @@ -72,9 +72,11 @@ export const mockExploreState = (options: any = {}) => { range, }; const split: boolean = options.split || false; + const syncedTimes: boolean = options.syncedTimes || false; const explore: ExploreState = { left, right, + syncedTimes, split, };