Commit dabc848e by Ivana Huckova Committed by GitHub

Explore: Synchronise time ranges in split mode (#19274)

* Explore: create connected sync button when screen is splitted

* Explore: create attachable button to TimePicker

* WIP/Explore: set up redux boilerplate for synced state

* WIP/Explore: add toggling functionality to sync buttons

* WIP/Explore: Fix styling issue

* First pass solution working

* Explore: Clean up, update comments

* Explore: refactore Timepicker, remove newly introduced class names

* Explore: refactore ExploreTimeControls

* Explore: more semantic variables naming

* Explore: run query on syncable item when synced times activated

* Explore: Add tooltip to sync times button

* Explore: Remove typo

* Explore: check exploreId

* Explore: refactor ExploreTimeControls

* Explore: refactor to include suggested changes

* Explore: Create TimeSyncButton component, update colors

* Explore: Toggle tooltip, use stylesFactory
parent 0f32e15a
// Libraries // Libraries
import React, { PureComponent, createRef } from 'react'; import React, { PureComponent, createRef } from 'react';
import { css } from 'emotion';
import memoizeOne from 'memoize-one';
import classNames from 'classnames';
// Components // Components
import { ButtonSelect } from '../Select/ButtonSelect'; import { ButtonSelect } from '../Select/ButtonSelect';
...@@ -11,15 +14,41 @@ import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper' ...@@ -11,15 +14,41 @@ import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'
import { isDateTime, DateTime } from '@grafana/data'; import { isDateTime, DateTime } from '@grafana/data';
import { rangeUtil } from '@grafana/data'; import { rangeUtil } from '@grafana/data';
import { rawToTimeRange } from './time'; import { rawToTimeRange } from './time';
import { withTheme } from '../../themes/ThemeContext';
// Types // Types
import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue } from '@grafana/data'; import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue, dateMath } from '@grafana/data';
import { dateMath } from '@grafana/data'; import { GrafanaTheme } from '../../types/theme';
import { Themeable } from '../../types';
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 { export interface Props extends Themeable {
value: TimeRange; value: TimeRange;
selectOptions: TimeOption[]; selectOptions: TimeOption[];
timeZone?: TimeZone; timeZone?: TimeZone;
timeSyncButton?: JSX.Element;
isSynced?: boolean;
onChange: (timeRange: TimeRange) => void; onChange: (timeRange: TimeRange) => void;
onMoveBackward: () => void; onMoveBackward: () => void;
onMoveForward: () => void; onMoveForward: () => void;
...@@ -70,7 +99,7 @@ const defaultZoomOutTooltip = () => { ...@@ -70,7 +99,7 @@ const defaultZoomOutTooltip = () => {
export interface State { export interface State {
isCustomOpen: boolean; isCustomOpen: boolean;
} }
export class TimePicker extends PureComponent<Props, State> { class UnThemedTimePicker extends PureComponent<Props, State> {
pickerTriggerRef = createRef<HTMLDivElement>(); pickerTriggerRef = createRef<HTMLDivElement>();
state: State = { state: State = {
...@@ -120,7 +149,19 @@ export class TimePicker extends PureComponent<Props, State> { ...@@ -120,7 +149,19 @@ export class TimePicker extends PureComponent<Props, State> {
}; };
render() { 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 { isCustomOpen } = this.state;
const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions); const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions);
const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value)); const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value));
...@@ -152,7 +193,10 @@ export class TimePicker extends PureComponent<Props, State> { ...@@ -152,7 +193,10 @@ export class TimePicker extends PureComponent<Props, State> {
</button> </button>
)} )}
<ButtonSelect <ButtonSelect
className="time-picker-button-select" className={classNames('time-picker-button-select', {
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: timeSyncButton,
[styles.timePickerSynced]: timeSyncButton ? isSynced : null,
})}
value={currentOption} value={currentOption}
label={label} label={label}
options={options} options={options}
...@@ -161,6 +205,9 @@ export class TimePicker extends PureComponent<Props, State> { ...@@ -161,6 +205,9 @@ export class TimePicker extends PureComponent<Props, State> {
iconClass={'fa fa-clock-o fa-fw'} iconClass={'fa fa-clock-o fa-fw'}
tooltipContent={<TimePickerTooltipContent timeRange={value} />} tooltipContent={<TimePickerTooltipContent timeRange={value} />}
/> />
{timeSyncButton}
{isAbsolute && ( {isAbsolute && (
<button className="btn navbar-button navbar-button--tight" onClick={onMoveForward}> <button className="btn navbar-button navbar-button--tight" onClick={onMoveForward}>
<i className="fa fa-chevron-right" /> <i className="fa fa-chevron-right" />
...@@ -195,3 +242,5 @@ const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => ( ...@@ -195,3 +242,5 @@ const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => (
function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean { function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean {
return range.raw.from === option.from && range.raw.to === option.to; return range.raw.from === option.from && range.raw.to === option.to;
} }
export const TimePicker = withTheme(UnThemedTimePicker);
...@@ -89,6 +89,7 @@ interface ExploreProps { ...@@ -89,6 +89,7 @@ interface ExploreProps {
mode: ExploreMode; mode: ExploreMode;
initialUI: ExploreUIState; initialUI: ExploreUIState;
isLive: boolean; isLive: boolean;
syncedTimes: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
graphResult?: GraphSeriesXY[]; graphResult?: GraphSeriesXY[];
loading?: boolean; loading?: boolean;
...@@ -178,7 +179,6 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -178,7 +179,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
onChangeTime = (rawRange: RawTimeRange) => { onChangeTime = (rawRange: RawTimeRange) => {
const { updateTimeRange, exploreId } = this.props; const { updateTimeRange, exploreId } = this.props;
updateTimeRange({ exploreId, rawRange }); updateTimeRange({ exploreId, rawRange });
}; };
...@@ -218,7 +218,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -218,7 +218,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
}; };
onUpdateTimeRange = (absoluteRange: AbsoluteTimeRange) => { onUpdateTimeRange = (absoluteRange: AbsoluteTimeRange) => {
const { updateTimeRange, exploreId } = this.props; const { exploreId, updateTimeRange } = this.props;
updateTimeRange({ exploreId, absoluteRange }); updateTimeRange({ exploreId, absoluteRange });
}; };
...@@ -263,6 +263,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -263,6 +263,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
showingTable, showingTable,
timeZone, timeZone,
queryResponse, queryResponse,
syncedTimes,
} = this.props; } = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
const styles = getStyles(); const styles = getStyles();
...@@ -326,6 +327,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -326,6 +327,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
<LogsContainer <LogsContainer
width={width} width={width}
exploreId={exploreId} exploreId={exploreId}
syncedTimes={syncedTimes}
onClickLabel={this.onClickLabel} onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning} onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning} onStopScanning={this.onStopScanning}
...@@ -350,7 +352,7 @@ const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl); ...@@ -350,7 +352,7 @@ const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial<ExploreProps> { function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial<ExploreProps> {
const explore = state.explore; const explore = state.explore;
const { split } = explore; const { split, syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId];
const timeZone = getTimeZone(state.user); const timeZone = getTimeZone(state.user);
const { const {
...@@ -421,6 +423,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia ...@@ -421,6 +423,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
absoluteRange, absoluteRange,
queryResponse, queryResponse,
originPanelId, originPanelId,
syncedTimes,
}; };
} }
......
...@@ -9,6 +9,7 @@ import { TimeRange, TimeOption, TimeZone, RawTimeRange, dateTimeForTimeZone } fr ...@@ -9,6 +9,7 @@ import { TimeRange, TimeOption, TimeZone, RawTimeRange, dateTimeForTimeZone } fr
// Components // Components
import { TimePicker } from '@grafana/ui'; import { TimePicker } from '@grafana/ui';
import { TimeSyncButton } from './TimeSyncButton';
// Utils & Services // Utils & Services
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
...@@ -18,6 +19,9 @@ export interface Props { ...@@ -18,6 +19,9 @@ export interface Props {
exploreId: ExploreId; exploreId: ExploreId;
range: TimeRange; range: TimeRange;
timeZone: TimeZone; timeZone: TimeZone;
splitted: boolean;
syncedTimes: boolean;
onChangeTimeSync: () => void;
onChangeTime: (range: RawTimeRange) => void; onChangeTime: (range: RawTimeRange) => void;
} }
...@@ -67,18 +71,18 @@ export class ExploreTimeControls extends Component<Props> { ...@@ -67,18 +71,18 @@ export class ExploreTimeControls extends Component<Props> {
}; };
render() { render() {
const { range, timeZone } = this.props; const { range, timeZone, splitted, syncedTimes, onChangeTimeSync } = this.props;
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : null;
const timePickerCommonProps = {
value: range,
onChange: this.onChangeTimePicker,
timeZone,
onMoveBackward: this.onMoveBack,
onMoveForward: this.onMoveForward,
onZoom: this.onZoom,
selectOptions: this.setActiveTimeOption(defaultSelectOptions, range.raw),
};
return ( return <TimePicker {...timePickerCommonProps} timeSyncButton={timeSyncButton} isSynced={syncedTimes} />;
<TimePicker
value={range}
onChange={this.onChangeTimePicker}
timeZone={timeZone}
onMoveBackward={this.onMoveBack}
onMoveForward={this.onMoveForward}
onZoom={this.onZoom}
selectOptions={this.setActiveTimeOption(defaultSelectOptions, range.raw)}
/>
);
} }
} }
...@@ -25,6 +25,7 @@ import { ...@@ -25,6 +25,7 @@ import {
splitClose, splitClose,
runQueries, runQueries,
splitOpen, splitOpen,
syncTimes,
changeRefreshInterval, changeRefreshInterval,
changeMode, changeMode,
clearOrigin, clearOrigin,
...@@ -60,6 +61,7 @@ interface StateProps { ...@@ -60,6 +61,7 @@ interface StateProps {
timeZone: TimeZone; timeZone: TimeZone;
selectedDatasource: DataSourceSelectItem; selectedDatasource: DataSourceSelectItem;
splitted: boolean; splitted: boolean;
syncedTimes: boolean;
refreshInterval: string; refreshInterval: string;
supportedModes: ExploreMode[]; supportedModes: ExploreMode[];
selectedMode: ExploreMode; selectedMode: ExploreMode;
...@@ -77,6 +79,7 @@ interface DispatchProps { ...@@ -77,6 +79,7 @@ interface DispatchProps {
runQueries: typeof runQueries; runQueries: typeof runQueries;
closeSplit: typeof splitClose; closeSplit: typeof splitClose;
split: typeof splitOpen; split: typeof splitOpen;
syncTimes: typeof syncTimes;
changeRefreshInterval: typeof changeRefreshInterval; changeRefreshInterval: typeof changeRefreshInterval;
changeMode: typeof changeMode; changeMode: typeof changeMode;
clearOrigin: typeof clearOrigin; clearOrigin: typeof clearOrigin;
...@@ -112,6 +115,11 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -112,6 +115,11 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
changeMode(exploreId, mode); changeMode(exploreId, mode);
}; };
onChangeTimeSync = () => {
const { syncTimes, exploreId } = this.props;
syncTimes(exploreId);
};
returnToPanel = async ({ withChanges = false } = {}) => { returnToPanel = async ({ withChanges = false } = {}) => {
const { originPanelId } = this.props; const { originPanelId } = this.props;
...@@ -148,6 +156,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -148,6 +156,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
timeZone, timeZone,
selectedDatasource, selectedDatasource,
splitted, splitted,
syncedTimes,
refreshInterval, refreshInterval,
onChangeTime, onChangeTime,
split, split,
...@@ -259,6 +268,9 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -259,6 +268,9 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
range={range} range={range}
timeZone={timeZone} timeZone={timeZone}
onChangeTime={onChangeTime} onChangeTime={onChangeTime}
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
/> />
</div> </div>
)} )}
...@@ -305,6 +317,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -305,6 +317,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => { const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
const splitted = state.explore.split; const splitted = state.explore.split;
const syncedTimes = state.explore.syncedTimes;
const exploreItem: ExploreItemState = state.explore[exploreId]; const exploreItem: ExploreItemState = state.explore[exploreId];
const { const {
datasourceInstance, datasourceInstance,
...@@ -343,6 +356,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps ...@@ -343,6 +356,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
isPaused, isPaused,
originPanelId, originPanelId,
queries, queries,
syncedTimes,
datasourceLoading, datasourceLoading,
}; };
}; };
...@@ -355,6 +369,7 @@ const mapDispatchToProps: DispatchProps = { ...@@ -355,6 +369,7 @@ const mapDispatchToProps: DispatchProps = {
runQueries, runQueries,
closeSplit: splitClose, closeSplit: splitClose,
split: splitOpen, split: splitOpen,
syncTimes,
changeMode: changeMode, changeMode: changeMode,
clearOrigin, clearOrigin,
}; };
......
...@@ -47,6 +47,7 @@ interface LogsContainerProps { ...@@ -47,6 +47,7 @@ interface LogsContainerProps {
isLive: boolean; isLive: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
range: TimeRange; range: TimeRange;
syncedTimes: boolean;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
isPaused: boolean; isPaused: boolean;
} }
...@@ -54,7 +55,6 @@ interface LogsContainerProps { ...@@ -54,7 +55,6 @@ interface LogsContainerProps {
export class LogsContainer extends PureComponent<LogsContainerProps> { export class LogsContainer extends PureComponent<LogsContainerProps> {
onChangeTime = (absoluteRange: AbsoluteTimeRange) => { onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
const { exploreId, updateTimeRange } = this.props; const { exploreId, updateTimeRange } = this.props;
updateTimeRange({ exploreId, absoluteRange }); updateTimeRange({ exploreId, absoluteRange });
}; };
......
import React from 'react';
import classNames from 'classnames';
import { css } from 'emotion';
import { GrafanaTheme, useTheme, stylesFactory } from '@grafana/ui';
//Components
import { Tooltip } from '@grafana/ui';
const getStyles = stylesFactory((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;
`,
};
});
interface TimeSyncButtonProps {
isSynced: boolean;
onClick: () => void;
}
export function TimeSyncButton(props: TimeSyncButtonProps) {
const { onClick, isSynced } = props;
const theme = useTheme();
const styles = getStyles(theme);
const syncTimesTooltip = () => {
const { isSynced } = props;
const tooltip = isSynced ? 'Unsync all views' : 'Sync all views to this time range';
return <>{tooltip}</>;
};
return (
<Tooltip content={syncTimesTooltip} placement="bottom">
<button
className={classNames('btn navbar-button navbar-button--attached', {
[styles.timePickerSynced]: isSynced,
})}
onClick={() => onClick()}
>
<i className="fa fa-link" />
</button>
</Tooltip>
);
}
...@@ -13,6 +13,7 @@ import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFact ...@@ -13,6 +13,7 @@ import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFact
export enum ActionTypes { export enum ActionTypes {
SplitOpen = 'explore/SPLIT_OPEN', SplitOpen = 'explore/SPLIT_OPEN',
ResetExplore = 'explore/RESET_EXPLORE', ResetExplore = 'explore/RESET_EXPLORE',
SyncTimes = 'explore/SYNC_TIMES',
} }
export interface SplitOpenAction { export interface SplitOpenAction {
type: ActionTypes.SplitOpen; type: ActionTypes.SplitOpen;
...@@ -26,6 +27,10 @@ export interface ResetExploreAction { ...@@ -26,6 +27,10 @@ export interface ResetExploreAction {
payload: {}; payload: {};
} }
export interface SyncTimesAction {
type: ActionTypes.SyncTimes;
payload: { syncedTimes: boolean };
}
/** Lower order actions /** Lower order actions
* *
*/ */
...@@ -165,6 +170,10 @@ export interface SplitOpenPayload { ...@@ -165,6 +170,10 @@ export interface SplitOpenPayload {
itemState: ExploreItemState; itemState: ExploreItemState;
} }
export interface SyncTimesPayload {
syncedTimes: boolean;
}
export interface ToggleTablePayload { export interface ToggleTablePayload {
exploreId: ExploreId; exploreId: ExploreId;
} }
...@@ -352,6 +361,7 @@ export const splitCloseAction = actionCreatorFactory<SplitCloseActionPayload>('e ...@@ -352,6 +361,7 @@ export const splitCloseAction = actionCreatorFactory<SplitCloseActionPayload>('e
*/ */
export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create(); export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create();
export const syncTimesAction = actionCreatorFactory<SyncTimesPayload>('explore/SYNC_TIMES').create();
/** /**
* Update state of Explores UI elements (panels visiblity and deduplication strategy) * Update state of Explores UI elements (panels visiblity and deduplication strategy)
*/ */
...@@ -410,4 +420,5 @@ export type HigherOrderAction = ...@@ -410,4 +420,5 @@ export type HigherOrderAction =
| ActionOf<SplitCloseActionPayload> | ActionOf<SplitCloseActionPayload>
| SplitOpenAction | SplitOpenAction
| ResetExploreAction | ResetExploreAction
| SyncTimesAction
| ActionOf<any>; | ActionOf<any>;
...@@ -71,6 +71,7 @@ import { ...@@ -71,6 +71,7 @@ import {
queryStreamUpdatedAction, queryStreamUpdatedAction,
queryStoreSubscriptionAction, queryStoreSubscriptionAction,
clearOriginAction, clearOriginAction,
syncTimesAction,
} from './actionTypes'; } from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
...@@ -182,12 +183,19 @@ export const updateTimeRange = (options: { ...@@ -182,12 +183,19 @@ export const updateTimeRange = (options: {
rawRange?: RawTimeRange; rawRange?: RawTimeRange;
absoluteRange?: AbsoluteTimeRange; absoluteRange?: AbsoluteTimeRange;
}): ThunkResult<void> => { }): ThunkResult<void> => {
return dispatch => { return (dispatch, getState) => {
dispatch(updateTime({ ...options })); const { syncedTimes } = getState().explore;
dispatch(runQueries(options.exploreId)); 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. * Change the refresh interval of Explore. Called from the Refresh picker.
*/ */
...@@ -675,6 +683,25 @@ export function splitOpen(): ThunkResult<void> { ...@@ -675,6 +683,25 @@ export function splitOpen(): ThunkResult<void> {
} }
/** /**
* 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<void> {
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, * Creates action to collapse graph/logs/table panel. When panel is collapsed,
* queries won't be run * queries won't be run
*/ */
......
...@@ -129,6 +129,7 @@ export const createEmptyQueryResponse = (): PanelData => ({ ...@@ -129,6 +129,7 @@ export const createEmptyQueryResponse = (): PanelData => ({
export const initialExploreItemState = makeExploreItemState(); export const initialExploreItemState = makeExploreItemState();
export const initialExploreState: ExploreState = { export const initialExploreState: ExploreState = {
split: null, split: null,
syncedTimes: false,
left: initialExploreItemState, left: initialExploreItemState,
right: initialExploreItemState, right: initialExploreItemState,
}; };
...@@ -727,6 +728,9 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA ...@@ -727,6 +728,9 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
case ActionTypes.SplitOpen: { case ActionTypes.SplitOpen: {
return { ...state, split: true, right: { ...action.payload.itemState } }; return { ...state, split: true, right: { ...action.payload.itemState } };
} }
case ActionTypes.SyncTimes: {
return { ...state, syncedTimes: action.payload.syncedTimes };
}
case ActionTypes.ResetExplore: { case ActionTypes.ResetExplore: {
if (action.payload.force || !Number.isInteger(state.left.originPanelId)) { if (action.payload.force || !Number.isInteger(state.left.originPanelId)) {
......
...@@ -131,6 +131,10 @@ export interface ExploreState { ...@@ -131,6 +131,10 @@ export interface ExploreState {
*/ */
split: boolean; 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). * Explore state of the left split (left is default in non-split view).
*/ */
left: ExploreItemState; left: ExploreItemState;
......
...@@ -72,9 +72,11 @@ export const mockExploreState = (options: any = {}) => { ...@@ -72,9 +72,11 @@ export const mockExploreState = (options: any = {}) => {
range, range,
}; };
const split: boolean = options.split || false; const split: boolean = options.split || false;
const syncedTimes: boolean = options.syncedTimes || false;
const explore: ExploreState = { const explore: ExploreState = {
left, left,
right, right,
syncedTimes,
split, split,
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment