Add fiscal years and search to time picker (#39073)

* Add search to time picker

* implement fiscal datemath

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Oscar Kilhed
2021-09-30 09:40:02 +02:00
committed by GitHub
parent 787e5e78dd
commit 738d5e499e
30 changed files with 457 additions and 164 deletions

View File

@@ -14,6 +14,7 @@ export class User {
login: string;
orgCount: number;
timezone: string;
fiscalYearStartMonth: number;
helpFlags1: number;
lightTheme: boolean;
hasEditPermissionInFolders: boolean;
@@ -30,6 +31,7 @@ export class User {
this.login = '';
this.orgCount = 0;
this.timezone = '';
this.fiscalYearStartMonth = 0;
this.helpFlags1 = 0;
this.lightTheme = false;
this.hasEditPermissionInFolders = false;

View File

@@ -302,7 +302,7 @@ describe('getTimeRangeFromUrl', () => {
it('should parse moment date', () => {
// convert date strings to moment object
const range = { from: dateTime('2020-10-22T10:44:33.615Z'), to: dateTime('2020-10-22T10:49:33.615Z') };
const result = getTimeRangeFromUrl(range, 'browser');
const result = getTimeRangeFromUrl(range, 'browser', 0);
expect(result.raw).toEqual(range);
});
@@ -311,7 +311,7 @@ describe('getTimeRangeFromUrl', () => {
from: dateTime('2020-10-22T10:00:00Z').valueOf().toString(),
to: dateTime('2020-10-22T11:00:00Z').valueOf().toString(),
};
const result = getTimeRangeFromUrl(range, 'browser');
const result = getTimeRangeFromUrl(range, 'browser', 0);
expect(result.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf());
expect(result.raw.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
@@ -323,7 +323,7 @@ describe('getTimeRangeFromUrl', () => {
from: dateTime('2020-10-22T10:00:00Z').toISOString(),
to: dateTime('2020-10-22T11:00:00Z').toISOString(),
};
const result = getTimeRangeFromUrl(range, 'browser');
const result = getTimeRangeFromUrl(range, 'browser', 0);
expect(result.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf());
expect(result.raw.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());

View File

@@ -341,10 +341,10 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSour
return queryKeys;
};
export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => {
export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange, fiscalYearStartMonth: number): TimeRange => {
return {
from: dateMath.parse(rawRange.from, false, timeZone as any)!,
to: dateMath.parse(rawRange.to, true, timeZone as any)!,
from: dateMath.parse(rawRange.from, false, timeZone as any, fiscalYearStartMonth)!,
to: dateMath.parse(rawRange.to, true, timeZone as any, fiscalYearStartMonth)!,
raw: rawRange,
};
};
@@ -387,7 +387,11 @@ const parseRawTime = (value: string | DateTime): TimeFragment | null => {
return null;
};
export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => {
export const getTimeRangeFromUrl = (
range: RawTimeRange,
timeZone: TimeZone,
fiscalYearStartMonth: number
): TimeRange => {
const raw = {
from: parseRawTime(range.from)!,
to: parseRawTime(range.to)!,

View File

@@ -70,6 +70,11 @@ export class DashNavTimeControls extends Component<Props> {
this.onRefresh();
};
onChangeFiscalYearStartMonth = (month: number) => {
this.props.dashboard.fiscalYearStartMonth = month;
this.onRefresh();
};
onZoom = () => {
appEvents.publish(new ZoomOutEvent(2));
};
@@ -81,6 +86,7 @@ export class DashNavTimeControls extends Component<Props> {
const timePickerValue = getTimeSrv().timeRange();
const timeZone = dashboard.getTimezone();
const fiscalYearStartMonth = dashboard.fiscalYearStartMonth;
const hideIntervalPicker = dashboard.panelInEdit?.isEditing;
return (
@@ -89,10 +95,12 @@ export class DashNavTimeControls extends Component<Props> {
value={timePickerValue}
onChange={this.onChangeTimePicker}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onMoveBackward={this.onMoveBack}
onMoveForward={this.onMoveForward}
onZoom={this.onZoom}
onChangeTimeZone={this.onChangeTimeZone}
onChangeFiscalYearStartMonth={this.onChangeFiscalYearStartMonth}
/>
<RefreshPicker
onIntervalChanged={this.onChangeRefreshInterval}

View File

@@ -60,7 +60,11 @@ export class TimeSrv {
// remember time at load so we can go back to it
this.timeAtLoad = cloneDeep(this.time);
const range = rangeUtil.convertRawToRange(this.time, this.dashboard?.getTimezone());
const range = rangeUtil.convertRawToRange(
this.time,
this.dashboard?.getTimezone(),
this.dashboard?.fiscalYearStartMonth
);
if (range.to.isBefore(range.from)) {
this.setTime(
@@ -327,8 +331,8 @@ export class TimeSrv {
const timezone = this.dashboard ? this.dashboard.getTimezone() : undefined;
return {
from: dateMath.parse(raw.from, false, timezone)!,
to: dateMath.parse(raw.to, true, timezone)!,
from: dateMath.parse(raw.from, false, timezone, this.dashboard?.fiscalYearStartMonth)!,
to: dateMath.parse(raw.to, true, timezone, this.dashboard?.fiscalYearStartMonth)!,
raw: raw,
};
}

View File

@@ -96,6 +96,7 @@ export class DashboardModel {
panels: PanelModel[];
panelInEdit?: PanelModel;
panelInView?: PanelModel;
fiscalYearStartMonth?: number;
private hasChangesThatAffectsAllPanels: boolean;
// ------------------
@@ -131,26 +132,27 @@ export class DashboardModel {
this.id = data.id || null;
this.uid = data.uid || null;
this.revision = data.revision;
this.title = data.title || 'No Title';
this.title = data.title ?? 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || 'dark';
this.timezone = data.timezone || '';
this.tags = data.tags ?? [];
this.style = data.style ?? 'dark';
this.timezone = data.timezone ?? '';
this.editable = data.editable !== false;
this.graphTooltip = data.graphTooltip || 0;
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.time = data.time ?? { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker ?? {};
this.liveNow = Boolean(data.liveNow);
this.templating = this.ensureListExist(data.templating);
this.annotations = this.ensureListExist(data.annotations);
this.refresh = data.refresh;
this.snapshot = data.snapshot;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.schemaVersion = data.schemaVersion ?? 0;
this.fiscalYearStartMonth = data.fiscalYearStartMonth ?? 0;
this.version = data.version ?? 0;
this.links = data.links ?? [];
this.gnetId = data.gnetId || null;
this.panels = map(data.panels || [], (panelData: any) => new PanelModel(panelData));
this.panels = map(data.panels ?? [], (panelData: any) => new PanelModel(panelData));
this.formatDate = this.formatDate.bind(this);
this.resetOriginalVariables(true);

View File

@@ -16,7 +16,7 @@ import {
lastUsedDatasourceKeyForOrgId,
parseUrlState,
} from 'app/core/utils/explore';
import { getTimeZone } from '../profile/state/selectors';
import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors';
import Explore from './Explore';
interface OwnProps {
@@ -99,13 +99,14 @@ const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, props: OwnProps) {
const urlState = parseUrlState(props.urlQuery);
const timeZone = getTimeZone(state.user);
const fiscalYearStartMonth = getFiscalYearStartMonth(state.user);
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
const initialRange = urlRange
? getTimeRangeFromUrlMemoized(urlRange, timeZone)
: getTimeRange(timeZone, DEFAULT_RANGE);
? getTimeRangeFromUrlMemoized(urlRange, timeZone, fiscalYearStartMonth)
: getTimeRange(timeZone, DEFAULT_RANGE, fiscalYearStartMonth);
return {
initialized: state.explore[props.exploreId]?.initialized,

View File

@@ -19,11 +19,13 @@ export interface Props {
hideText?: boolean;
range: TimeRange;
timeZone: TimeZone;
fiscalYearStartMonth: number;
splitted: boolean;
syncedTimes: boolean;
onChangeTimeSync: () => void;
onChangeTime: (range: RawTimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
onChangeFiscalYearStartMonth: (fiscalYearStartMonth: number) => void;
}
export class ExploreTimeControls extends Component<Props> {
@@ -63,11 +65,22 @@ export class ExploreTimeControls extends Component<Props> {
};
render() {
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText, onChangeTimeZone } = this.props;
const {
range,
timeZone,
fiscalYearStartMonth,
splitted,
syncedTimes,
onChangeTimeSync,
hideText,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
} = this.props;
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
const timePickerCommonProps = {
value: range,
timeZone,
fiscalYearStartMonth,
onMoveBackward: this.onMoveBack,
onMoveForward: this.onMoveForward,
onZoom: this.onZoom,
@@ -81,6 +94,7 @@ export class ExploreTimeControls extends Component<Props> {
isSynced={syncedTimes}
onChange={this.onChangeTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
);
}

View File

@@ -12,8 +12,8 @@ import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { changeDatasource } from './state/datasource';
import { splitClose, splitOpen } from './state/main';
import { syncTimes, changeRefreshInterval } from './state/time';
import { getTimeZone } from '../profile/state/selectors';
import { updateTimeZoneForSession } from '../profile/state/reducers';
import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors';
import { updateFiscalYearStartMonthForSession, updateTimeZoneForSession } from '../profile/state/reducers';
import { ExploreTimeControls } from './ExploreTimeControls';
import { LiveTailButton } from './LiveTailButton';
import { RunButton } from './RunButton';
@@ -65,6 +65,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
loading,
range,
timeZone,
fiscalYearStartMonth,
splitted,
syncedTimes,
refreshInterval,
@@ -75,6 +76,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
isPaused,
containerWidth,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
} = this.props;
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
@@ -158,12 +160,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
exploreId={exploreId}
range={range}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTime={onChangeTime}
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
)}
@@ -230,6 +234,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
loading,
range,
timeZone: getTimeZone(state.user),
fiscalYearStartMonth: getFiscalYearStartMonth(state.user),
splitted: isSplit(state),
refreshInterval,
hasLiveOption,
@@ -250,6 +255,7 @@ const mapDispatchToProps = {
split: splitOpen,
syncTimes,
onChangeTimeZone: updateTimeZoneForSession,
onChangeFiscalYearStartMonth: updateFiscalYearStartMonthForSession,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -24,7 +24,7 @@ import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
// Types
import { ThunkResult } from 'app/types';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { getDataSourceSrv } from '@grafana/runtime';
import { getRichHistory } from '../../../core/utils/richHistory';
import { richHistoryUpdatedAction } from './main';
@@ -153,7 +153,8 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
}
const timeZone = getTimeZone(getState().user);
const range = getTimeRangeFromUrl(urlRange, timeZone);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
const range = getTimeRangeFromUrl(urlRange, timeZone, fiscalYearStartMonth);
// commit changes based on the diff of new url vs old url

View File

@@ -12,7 +12,7 @@ import { RefreshPicker } from '@grafana/ui';
import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore';
import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { runQueries } from './query';
@@ -78,6 +78,7 @@ export const updateTime = (config: {
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
const itemState = getState().explore[exploreId]!;
const timeZone = getTimeZone(getState().user);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
const { range: rangeInState } = itemState;
let rawRange: RawTimeRange = rangeInState.raw;
@@ -92,7 +93,7 @@ export const updateTime = (config: {
rawRange = actionRange;
}
const range = getTimeRange(timeZone, rawRange);
const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth);
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
getTimeSrv().init(

View File

@@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core';
export interface UserState {
orgId: number;
timeZone: TimeZone;
fiscalYearStartMonth: number;
user: UserDTO | null;
teams: Team[];
orgs: UserOrg[];
@@ -22,6 +23,7 @@ export interface UserState {
export const initialUserState: UserState = {
orgId: config.bootData.user.orgId,
timeZone: config.bootData.user.timezone,
fiscalYearStartMonth: 0,
orgsAreLoading: false,
sessionsAreLoading: false,
teamsAreLoading: false,
@@ -39,6 +41,9 @@ export const slice = createSlice({
updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => {
state.timeZone = action.payload.timeZone;
},
updateFiscalYearStartMonth: (state, action: PayloadAction<{ fiscalYearStartMonth: number }>) => {
state.fiscalYearStartMonth = action.payload.fiscalYearStartMonth;
},
setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => {
state.isUpdating = action.payload.updating;
},
@@ -87,6 +92,13 @@ export const slice = createSlice({
},
});
export const updateFiscalYearStartMonthForSession = (fiscalYearStartMonth: number): ThunkResult<void> => {
return async (dispatch) => {
set(contextSrv, 'user.fiscalYearStartMonth', fiscalYearStartMonth);
dispatch(updateFiscalYearStartMonth({ fiscalYearStartMonth }));
};
};
export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void> => {
return async (dispatch) => {
if (!isString(timeZone) || isEmpty(timeZone)) {
@@ -109,6 +121,7 @@ export const {
initLoadSessions,
sessionsLoaded,
updateTimeZone,
updateFiscalYearStartMonth,
} = slice.actions;
export const userReducer = slice.reducer;

View File

@@ -1,3 +1,4 @@
import { UserState } from './reducers';
export const getTimeZone = (state: UserState) => state.timeZone;
export const getFiscalYearStartMonth = (state: UserState) => state.fiscalYearStartMonth;