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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 457 additions and 164 deletions

View File

@ -135,4 +135,71 @@ describe('DateMath', () => {
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
});
});
describe('Round to fiscal start/end', () => {
it('Should round to start of fiscal year when datetime is the same year as the start of the fiscal year', () => {
let date = dateMath.roundToFiscal(1, dateTime([2021, 3, 5]), 'y', false);
let expected = dateTime([2021, 1, 1]);
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to start of fiscal year when datetime is the next year from the start of the fiscal year', () => {
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 2]), 'y', false);
let expected = dateTime([2021, 1, 1]);
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to start of fiscal year when datetime is on a leap day', () => {
let date = dateMath.roundToFiscal(1, dateTime([2020, 1, 29]), 'y', false);
let expected = dateTime([2020, 1, 1]);
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to end of fiscal year when datetime is the same year as the start of the fiscal year', () => {
let date = dateMath.roundToFiscal(1, dateTime([2021, 5, 2]), 'y', true);
let expected = dateTime([2022, 0, 1]).endOf('M');
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to end of fiscal year when datetime is the next year from the start of the fiscal year', () => {
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'y', true);
let expected = dateTime([2022, 0, 1]).endOf('M');
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to end of fiscal year when datetime is on a leap day', () => {
let date = dateMath.roundToFiscal(1, dateTime([2020, 1, 29]), 'y', true);
let expected = dateTime([2021, 0, 1]).endOf('M');
expect(date!.valueOf()).toEqual(expected.valueOf());
});
//fq1 = 2021-02-01 - 2021-04-30
//fq2 = 2021-05-01 - 2021-07-31
//fq4 = 2021-08-01 - 2021-10-31
//fq5 = 2021-11-01 - 2022-01-31
it('Should round to start of q2 when one month into q2', () => {
let date = dateMath.roundToFiscal(1, dateTime([2021, 6, 1]), 'Q', false);
let expected = dateTime([2021, 4, 1]);
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to start of q4 when datetime is in next year from fiscal year start', () => {
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'Q', false);
let expected = dateTime([2021, 10, 1]);
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to end of q2 when one month into q2', () => {
let date = dateMath.roundToFiscal(1, dateTime([2021, 6, 1]), 'Q', true);
let expected = dateTime([2021, 6, 1]).endOf('M');
expect(date!.valueOf()).toEqual(expected.valueOf());
});
it('Should round to end of q4 when datetime is in next year from fiscal year start', () => {
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'Q', true);
let expected = dateTime([2022, 0, 31]).endOf('M');
expect(date!.valueOf()).toEqual(expected.valueOf());
});
});
});

View File

@ -2,7 +2,7 @@ import { includes, isDate } from 'lodash';
import { DateTime, dateTime, dateTimeForTimeZone, ISO_8601, isDateTime, DurationUnit } from './moment_wrapper';
import { TimeZone } from '../types/index';
const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'Q'];
export function isMathString(text: string | DateTime | Date): boolean {
if (!text) {
@ -26,7 +26,8 @@ export function isMathString(text: string | DateTime | Date): boolean {
export function parse(
text?: string | DateTime | Date | null,
roundUp?: boolean,
timezone?: TimeZone
timezone?: TimeZone,
fiscalYearStartMonth?: number
): DateTime | undefined {
if (!text) {
return undefined;
@ -67,7 +68,7 @@ export function parse(
return time;
}
return parseDateMath(mathString, time, roundUp);
return parseDateMath(mathString, time, roundUp, fiscalYearStartMonth);
}
}
@ -96,7 +97,12 @@ export function isValid(text: string | DateTime): boolean {
* @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
*/
// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any`
export function parseDateMath(mathString: string, time: any, roundUp?: boolean): DateTime | undefined {
export function parseDateMath(
mathString: string,
time: any,
roundUp?: boolean,
fiscalYearStartMonth = 0
): DateTime | undefined {
const strippedMathString = mathString.replace(/\s/g, '');
const dateTime = time;
let i = 0;
@ -107,6 +113,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
let type;
let num;
let unit;
let isFiscal = false;
if (c === '/') {
type = 0;
@ -121,7 +128,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
if (isNaN(parseInt(strippedMathString.charAt(i), 10))) {
num = 1;
} else if (strippedMathString.length === 2) {
num = strippedMathString.charAt(i);
num = parseInt(strippedMathString.charAt(i), 10);
} else {
const numFrom = i;
while (!isNaN(parseInt(strippedMathString.charAt(i), 10))) {
@ -141,14 +148,27 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
}
unit = strippedMathString.charAt(i++);
if (unit === 'f') {
unit = strippedMathString.charAt(i++);
isFiscal = true;
}
if (!includes(units, unit)) {
return undefined;
} else {
if (type === 0) {
if (roundUp) {
dateTime.endOf(unit);
if (isFiscal) {
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
} else {
dateTime.endOf(unit);
}
} else {
dateTime.startOf(unit);
if (isFiscal) {
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
} else {
dateTime.startOf(unit);
}
}
} else if (type === 1) {
dateTime.add(num, unit);
@ -159,3 +179,24 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
}
return dateTime;
}
export function roundToFiscal(fyStartMonth: number, dateTime: any, unit: string, roundUp: boolean | undefined) {
switch (unit) {
case 'y':
if (roundUp) {
roundToFiscal(fyStartMonth, dateTime, unit, false).add(11, 'M').endOf('M');
} else {
dateTime.subtract((dateTime.month() - fyStartMonth + 12) % 12, 'M').startOf('M');
}
return dateTime;
case 'Q':
if (roundUp) {
roundToFiscal(fyStartMonth, dateTime, unit, false).add(2, 'M').endOf('M');
} else {
dateTime.subtract((dateTime.month() - fyStartMonth + 3) % 3, 'M').startOf('M');
}
return dateTime;
default:
return undefined;
}
}

View File

@ -19,6 +19,7 @@ export interface DateTimeOptionsWhenParsing extends DateTimeOptions {
* the returned DateTime value will be 06:00:00.
*/
roundUp?: boolean;
fiscalYearStartMonth?: number;
}
type DateTimeParser<T extends DateTimeOptions = DateTimeOptions> = (value: DateTimeInput, options?: T) => DateTime;
@ -56,7 +57,7 @@ const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateT
return moment() as DateTime;
}
const parsed = parse(value, options?.roundUp, options?.timeZone);
const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth);
return parsed || (moment() as DateTime);
}

View File

@ -40,7 +40,9 @@ const rangeOptions: TimeOption[] = [
},
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
{ from: 'now-1Q/fQ', to: 'now-1Q/fQ', display: 'Previous fiscal quarter' },
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
{ from: 'now-1y/fy', to: 'now-1y/fy', display: 'Previous fiscal year' },
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
@ -58,6 +60,10 @@ const rangeOptions: TimeOption[] = [
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
{ from: 'now/fQ', to: 'now', display: 'This fiscal quarter so far' },
{ from: 'now/fQ', to: 'now/fQ', display: 'This fiscal quarter' },
{ from: 'now/fy', to: 'now', display: 'This fiscal year so far' },
{ from: 'now/fy', to: 'now/fy', display: 'This fiscal year' },
];
const hiddenRangeOptions: TimeOption[] = [
@ -192,9 +198,9 @@ export const describeTimeRangeAbbreviation = (range: TimeRange, timeZone?: TimeZ
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
};
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
const from = dateTimeParse(raw.from, { roundUp: false, timeZone });
const to = dateTimeParse(raw.to, { roundUp: true, timeZone });
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone, fiscalYearStartMonth?: number): TimeRange => {
const from = dateTimeParse(raw.from, { roundUp: false, timeZone, fiscalYearStartMonth });
const to = dateTimeParse(raw.to, { roundUp: true, timeZone, fiscalYearStartMonth });
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
return { from, to, raw };
@ -210,6 +216,15 @@ function isRelativeTime(v: DateTime | string) {
return false;
}
export function isFiscal(timeRange: TimeRange) {
if (typeof timeRange.raw.from === 'string' && timeRange.raw.from.indexOf('f') > 0) {
return true;
} else if (typeof timeRange.raw.to === 'string' && timeRange.raw.to.indexOf('f') > 0) {
return true;
}
return false;
}
export function isRelativeTimeRange(raw: RawTimeRange): boolean {
return isRelativeTime(raw.from) || isRelativeTime(raw.to);
}

View File

@ -11,10 +11,10 @@ export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
e2e.components.TimePicker.openButton().click();
if (zone) {
e2e().contains('button', 'Change time zone').click();
e2e().contains('button', 'Change time settings').click();
selectOption({
clickToOpen: false,
clickToOpen: true,
container: e2e.components.TimeZonePicker.container(),
optionText: zone,
});

View File

@ -5,7 +5,7 @@ import { useStyles2 } from '../../../themes';
import { Button } from '../../Button';
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList';
import { quickOptions } from '../rangeOptions';
import { quickOptions } from '../options';
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';
import {

View File

@ -7,7 +7,7 @@ import { Icon } from '../Icon/Icon';
import { getInputStyles } from '../Input/Input';
import { TimePickerButtonLabel } from './TimeRangePicker';
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
import { otherOptions, quickOptions } from './rangeOptions';
import { quickOptions } from './options';
import { selectors } from '@grafana/e2e-selectors';
import { stylesFactory } from '../../themes';
@ -100,7 +100,6 @@ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
timeZone={timeZone}
value={isValidTimeRange(value) ? (value as TimeRange) : getDefaultTimeRange()}
onChange={onRangeChange}
otherOptions={otherOptions}
quickOptions={quickOptions}
onChangeTimeZone={onChangeTimeZone}
className={styles.content}

View File

@ -23,7 +23,7 @@ import {
dateMath,
} from '@grafana/data';
import { Themeable } from '../../types';
import { otherOptions, quickOptions } from './rangeOptions';
import { quickOptions } from './options';
import { ButtonGroup, ToolbarButton } from '../Button';
import { selectors } from '@grafana/e2e-selectors';
@ -32,10 +32,12 @@ export interface TimeRangePickerProps extends Themeable {
hideText?: boolean;
value: TimeRange;
timeZone?: TimeZone;
fiscalYearStartMonth?: number;
timeSyncButton?: JSX.Element;
isSynced?: boolean;
onChange: (timeRange: TimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
onChangeFiscalYearStartMonth?: (month: number) => void;
onMoveBackward: () => void;
onMoveForward: () => void;
onZoom: () => void;
@ -75,11 +77,13 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
onMoveForward,
onZoom,
timeZone,
fiscalYearStartMonth,
timeSyncButton,
isSynced,
theme,
history,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
hideQuickRanges,
} = this.props;
@ -117,13 +121,14 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
<TimePickerContent
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
value={value}
onChange={this.onChange}
otherOptions={otherOptions}
quickOptions={quickOptions}
history={history}
showHistory
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
hideQuickRanges={hideQuickRanges}
/>
</ClickOutsideWrapper>

View File

@ -38,14 +38,12 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => {
renderComponent({ value: absoluteValue });
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
});
it('renders without relative picker', () => {
renderComponent({ value: absoluteValue, hideQuickRanges: true });
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
});
it('renders with timezone picker', () => {
@ -86,14 +84,12 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => {
renderComponent({ value: absoluteValue, isFullscreen: false });
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
});
it('renders without relative picker', () => {
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
});
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
@ -139,6 +135,10 @@ function renderComponent({
<TimePickerContentWithScreenSize
onChangeTimeZone={noop}
onChange={noop}
quickOptions={[
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
]}
timeZone="utc"
value={value}
isFullscreen={isFullscreen}

View File

@ -12,6 +12,7 @@ import { TimeRangeList } from './TimeRangeList';
import { TimePickerFooter } from './TimePickerFooter';
import { getFocusStyles } from '../../../themes/mixins';
import { selectors } from '@grafana/e2e-selectors';
import { FilterInput } from '../..';
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
return {
@ -46,11 +47,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRang
rightSide: css`
width: 40% !important;
border-right: ${isReversed ? `1px solid ${theme.colors.border.weak}` : 'none'};
display: flex;
flex-direction: column;
@media only screen and (max-width: ${theme.breakpoints.values.lg}px) {
width: 100% !important;
}
`,
timeRangeFilter: css`
padding: ${theme.spacing(1)};
`,
spacing: css`
margin-top: 16px;
`,
@ -127,9 +132,10 @@ interface Props {
value: TimeRange;
onChange: (timeRange: TimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
onChangeFiscalYearStartMonth?: (month: number) => void;
timeZone?: TimeZone;
fiscalYearStartMonth?: number;
quickOptions?: TimeOption[];
otherOptions?: TimeOption[];
history?: TimeRange[];
showHistory?: boolean;
className?: string;
@ -150,11 +156,11 @@ interface FormProps extends Omit<Props, 'history'> {
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (props) => {
const {
quickOptions = [],
otherOptions = [],
isReversed,
isFullscreen,
hideQuickRanges,
timeZone,
fiscalYearStartMonth,
value,
onChange,
history,
@ -162,6 +168,7 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
className,
hideTimeZone,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
} = props;
const isHistoryEmpty = !history?.length;
const isContainerTall =
@ -169,7 +176,10 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
const theme = useTheme2();
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
const historyOptions = mapToHistoryOptions(history, timeZone);
const timeOption = useTimeOption(value.raw, otherOptions, quickOptions);
const timeOption = useTimeOption(value.raw, quickOptions);
const [searchTerm, setSearchQuery] = useState('');
const filteredQuickOptions = quickOptions.filter((o) => o.display.toLowerCase().includes(searchTerm.toLowerCase()));
const onChangeTimeOption = (timeOption: TimeOption) => {
return onChange(mapOptionToTimeRange(timeOption));
@ -179,26 +189,23 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
<div id="TimePickerContent" className={cx(styles.container, className)}>
<div className={styles.body}>
{(!isFullscreen || !hideQuickRanges) && (
<CustomScrollbar className={styles.rightSide}>
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
{!hideQuickRanges && (
<>
<TimeRangeList
title="Relative time ranges"
options={quickOptions}
onChange={onChangeTimeOption}
value={timeOption}
/>
<div className={styles.spacing} />
<TimeRangeList
title="Other quick ranges"
options={otherOptions}
onChange={onChangeTimeOption}
value={timeOption}
/>
</>
)}
</CustomScrollbar>
<div className={styles.rightSide}>
<div className={styles.timeRangeFilter}>
<FilterInput
width={0}
autoFocus={true}
value={searchTerm}
onChange={setSearchQuery}
placeholder={'Search quick ranges'}
/>
</div>
<CustomScrollbar>
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
{!hideQuickRanges && (
<TimeRangeList options={filteredQuickOptions} onChange={onChangeTimeOption} value={timeOption} />
)}
</CustomScrollbar>
</div>
)}
{isFullscreen && (
<div className={styles.leftSide}>
@ -206,7 +213,14 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
</div>
)}
</div>
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
{!hideTimeZone && isFullscreen && (
<TimePickerFooter
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
)}
</div>
);
};
@ -268,7 +282,7 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
};
const FullScreenForm: React.FC<FormProps> = (props) => {
const { onChange } = props;
const { onChange, value, timeZone, fiscalYearStartMonth, isReversed, historyOptions } = props;
const theme = useTheme2();
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
const onChangeTimeOption = (timeOption: TimeOption) => {
@ -282,18 +296,19 @@ const FullScreenForm: React.FC<FormProps> = (props) => {
<TimePickerTitle>Absolute time range</TimePickerTitle>
</div>
<TimeRangeForm
value={props.value}
timeZone={props.timeZone}
onApply={props.onChange}
value={value}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onApply={onChange}
isFullscreen={true}
isReversed={props.isReversed}
isReversed={isReversed}
/>
</div>
{props.showHistory && (
<div className={styles.recent}>
<TimeRangeList
title="Recently used absolute ranges"
options={props.historyOptions || []}
options={historyOptions || []}
onChange={onChangeTimeOption}
placeholderEmpty={<EmptyRecentList />}
/>
@ -338,23 +353,13 @@ function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOpt
EmptyRecentList.displayName = 'EmptyRecentList';
const useTimeOption = (
raw: RawTimeRange,
quickOptions: TimeOption[],
otherOptions: TimeOption[]
): TimeOption | undefined => {
const useTimeOption = (raw: RawTimeRange, quickOptions: TimeOption[]): TimeOption | undefined => {
return useMemo(() => {
if (!rangeUtil.isRelativeTimeRange(raw)) {
return;
}
const quickOption = quickOptions.find((option) => {
return quickOptions.find((option) => {
return option.from === raw.from && option.to === raw.to;
});
if (quickOption) {
return quickOption;
}
return otherOptions.find((option) => {
return option.from === raw.from && option.to === raw.to;
});
}, [raw, otherOptions, quickOptions]);
}, [raw, quickOptions]);
};

View File

@ -9,18 +9,29 @@ import { Button } from '../../Button';
import { TimeZonePicker } from '../TimeZonePicker';
import { isString } from 'lodash';
import { selectors } from '@grafana/e2e-selectors';
import { Field, RadioButtonGroup, Select } from '../..';
import { monthOptions } from '../options';
interface Props {
timeZone?: TimeZone;
fiscalYearStartMonth?: number;
timestamp?: number;
onChangeTimeZone: (timeZone: TimeZone) => void;
onChangeFiscalYearStartMonth?: (month: number) => void;
}
export const TimePickerFooter: FC<Props> = (props) => {
const { timeZone, timestamp = Date.now(), onChangeTimeZone } = props;
const {
timeZone,
fiscalYearStartMonth,
timestamp = Date.now(),
onChangeTimeZone,
onChangeFiscalYearStartMonth,
} = props;
const [isEditing, setEditing] = useState(false);
const [editMode, setEditMode] = useState('tz');
const onToggleChangeTz = useCallback(
const onToggleChangeTimeSettings = useCallback(
(event?: React.MouseEvent) => {
if (event) {
event.stopPropagation();
@ -43,42 +54,72 @@ export const TimePickerFooter: FC<Props> = (props) => {
return null;
}
if (isEditing) {
return (
<div className={cx(style.container, style.editContainer)}>
<section aria-label={selectors.components.TimeZonePicker.container} className={style.timeZoneContainer}>
<TimeZonePicker
includeInternal={true}
onChange={(timeZone) => {
onToggleChangeTz();
if (isString(timeZone)) {
onChangeTimeZone(timeZone);
}
}}
autoFocus={true}
onBlur={onToggleChangeTz}
/>
</section>
</div>
);
}
return (
<section aria-label="Time zone selection" className={style.container}>
<div className={style.timeZoneContainer}>
<div className={style.timeZone}>
<TimeZoneTitle title={info.name} />
<div className={style.spacer} />
<TimeZoneDescription info={info} />
<div>
<section aria-label="Time zone selection" className={style.container}>
<div className={style.timeZoneContainer}>
<div className={style.timeZone}>
<TimeZoneTitle title={info.name} />
<div className={style.spacer} />
<TimeZoneDescription info={info} />
</div>
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
</div>
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
</div>
<div className={style.spacer} />
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
Change time zone
</Button>
</section>
<div className={style.spacer} />
<Button variant="secondary" onClick={onToggleChangeTimeSettings} size="sm">
Change time settings
</Button>
</section>
{isEditing ? (
<div className={style.editContainer}>
<div>
<RadioButtonGroup
value={editMode}
options={[
{ label: 'Time Zone', value: 'tz' },
{ label: 'Fiscal year', value: 'fy' },
]}
onChange={setEditMode}
></RadioButtonGroup>
</div>
{editMode === 'tz' ? (
<section
aria-label={selectors.components.TimeZonePicker.container}
className={cx(style.timeZoneContainer, style.timeSettingContainer)}
>
<TimeZonePicker
includeInternal={true}
onChange={(timeZone) => {
onToggleChangeTimeSettings();
if (isString(timeZone)) {
onChangeTimeZone(timeZone);
}
}}
onBlur={onToggleChangeTimeSettings}
/>
</section>
) : (
<section
aria-label={selectors.components.TimeZonePicker.container}
className={cx(style.timeZoneContainer, style.timeSettingContainer)}
>
<Field className={style.fiscalYearField} label={'Fiscal year start month'}>
<Select
value={fiscalYearStartMonth}
options={monthOptions}
onChange={(value) => {
if (onChangeFiscalYearStartMonth) {
onChangeFiscalYearStartMonth(value.value ?? 0);
}
}}
/>
</Field>
</section>
)}
</div>
) : null}
</div>
);
};
@ -93,11 +134,21 @@ const getStyle = stylesFactory((theme: GrafanaTheme2) => {
align-items: center;
`,
editContainer: css`
border-top: 1px solid ${theme.colors.border.weak};
padding: 11px;
justify-content: space-between;
align-items: center;
padding: 7px;
`,
spacer: css`
margin-left: 7px;
`,
timeSettingContainer: css`
padding-top: ${theme.spacing(1)};
`,
fiscalYearField: css`
margin-bottom: 0px;
`,
timeZoneContainer: css`
display: flex;
flex-direction: row;

View File

@ -1,8 +1,10 @@
import { css } from '@emotion/css';
import {
dateMath,
DateTime,
dateTimeFormat,
dateTimeParse,
GrafanaTheme2,
isDateTime,
rangeUtil,
RawTimeRange,
@ -11,6 +13,8 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
import { Icon, Tooltip } from '../..';
import { useStyles2 } from '../../..';
import { Button } from '../../Button';
import { Field } from '../../Forms/Field';
import { Input } from '../../Input/Input';
@ -21,6 +25,7 @@ interface Props {
value: TimeRange;
onApply: (range: TimeRange) => void;
timeZone?: TimeZone;
fiscalYearStartMonth?: number;
roundup?: boolean;
isReversed?: boolean;
}
@ -37,8 +42,9 @@ const ERROR_MESSAGES = {
};
export const TimeRangeForm: React.FC<Props> = (props) => {
const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps, isReversed } = props;
const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps, isReversed, fiscalYearStartMonth } = props;
const [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone);
const style = useStyles2(getStyles);
const [from, setFrom] = useState<InputState>(fromValue);
const [to, setTo] = useState<InputState>(toValue);
@ -77,11 +83,11 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
}
const raw: RawTimeRange = { from: from.value, to: to.value };
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
const timeRange = rangeUtil.convertRawToRange(raw, timeZone, fiscalYearStartMonth);
onApplyFromProps(timeRange);
},
[from.invalid, from.value, onApplyFromProps, timeZone, to.invalid, to.value]
[from.invalid, from.value, onApplyFromProps, timeZone, to.invalid, to.value, fiscalYearStartMonth]
);
const onChange = useCallback(
@ -93,29 +99,47 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
[timeZone]
);
const fiscalYear = rangeUtil.convertRawToRange({ from: 'now/fy', to: 'now/fy' }, timeZone, fiscalYearStartMonth);
const fyTooltip = (
<div className={style.tooltip}>
{rangeUtil.isFiscal(value) ? (
<Tooltip content={`Fiscal year: ${fiscalYear.from.format('MMM-DD')} - ${fiscalYear.to.format('MMM-DD')}`}>
<Icon name="info-circle" />
</Tooltip>
) : null}
</div>
);
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
return (
<div aria-label="Absolute time ranges">
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
<Input
onClick={(event) => event.stopPropagation()}
onFocus={onFocus}
onChange={(event) => onChange(event.currentTarget.value, to.value)}
addonAfter={icon}
aria-label={selectors.components.TimePicker.fromField}
value={from.value}
/>
<div className={style.fieldContainer}>
<Input
onClick={(event) => event.stopPropagation()}
onFocus={onFocus}
onChange={(event) => onChange(event.currentTarget.value, to.value)}
addonAfter={icon}
aria-label={selectors.components.TimePicker.fromField}
value={from.value}
/>
{fyTooltip}
</div>
</Field>
<Field label="To" invalid={to.invalid} error={to.errorMessage}>
<Input
onClick={(event) => event.stopPropagation()}
onFocus={onFocus}
onChange={(event) => onChange(from.value, event.currentTarget.value)}
addonAfter={icon}
aria-label={selectors.components.TimePicker.toField}
value={to.value}
/>
<div className={style.fieldContainer}>
<Input
onClick={(event) => event.stopPropagation()}
onFocus={onFocus}
onChange={(event) => onChange(from.value, event.currentTarget.value)}
addonAfter={icon}
aria-label={selectors.components.TimePicker.toField}
value={to.value}
/>
{fyTooltip}
</div>
</Field>
<Button data-testid={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
Apply time range
@ -185,3 +209,15 @@ function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean
const parsed = dateTimeParse(value, { roundUp, timeZone });
return parsed.isValid();
}
function getStyles(theme: GrafanaTheme2) {
return {
fieldContainer: css`
display: flex;
`,
tooltip: css`
padding-left: ${theme.spacing(1)};
padding-top: ${theme.spacing(0.5)};
`,
};
}

View File

@ -26,7 +26,7 @@ const getOptionsStyles = stylesFactory(() => {
});
interface Props {
title: string;
title?: string;
options: TimeOption[];
value?: TimeOption;
onChange: (option: TimeOption) => void;
@ -69,7 +69,7 @@ const Options: React.FC<Props> = ({ options, value, onChange, title }) => {
value={option}
selected={isEqual(option, value)}
onSelect={onChange}
name={title}
name={title ?? 'Time ranges'}
/>
))}
</ul>

View File

@ -43,7 +43,6 @@ export const TimeZonePicker: React.FC<Props> = (props) => {
return (
<Select
menuShouldPortal
value={selected}
placeholder="Type to search (country, city, abbreviation)"
autoFocus={autoFocus}

View File

@ -1,4 +1,4 @@
import { TimeOption } from '@grafana/data';
import { SelectableValue, TimeOption } from '@grafana/data';
export const quickOptions: TimeOption[] = [
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
@ -17,15 +17,14 @@ export const quickOptions: TimeOption[] = [
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
];
export const otherOptions: TimeOption[] = [
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' },
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday' },
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week' },
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
{ from: 'now-1Q/fQ', to: 'now-1Q/fQ', display: 'Previous fiscal quarter' },
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
{ from: 'now-1y/fy', to: 'now-1y/fy', display: 'Previous fiscal year' },
{ from: 'now/d', to: 'now/d', display: 'Today' },
{ from: 'now/d', to: 'now', display: 'Today so far' },
{ from: 'now/w', to: 'now/w', display: 'This week' },
@ -34,4 +33,23 @@ export const otherOptions: TimeOption[] = [
{ from: 'now/M', to: 'now', display: 'This month so far' },
{ from: 'now/y', to: 'now/y', display: 'This year' },
{ from: 'now/y', to: 'now', display: 'This year so far' },
{ from: 'now/fQ', to: 'now', display: 'This fiscal quarter so far' },
{ from: 'now/fQ', to: 'now/fQ', display: 'This fiscal quarter' },
{ from: 'now/fy', to: 'now', display: 'This fiscal year so far' },
{ from: 'now/fy', to: 'now/fy', display: 'This fiscal year' },
];
export const monthOptions: Array<SelectableValue<number>> = [
{ label: 'January', value: 0 },
{ label: 'February', value: 1 },
{ label: 'March', value: 2 },
{ label: 'April', value: 3 },
{ label: 'May', value: 4 },
{ label: 'June', value: 5 },
{ label: 'July', value: 6 },
{ label: 'August', value: 7 },
{ label: 'September', value: 8 },
{ label: 'October', value: 9 },
{ label: 'November', value: 10 },
{ label: 'December', value: 11 },
];

View File

@ -14,7 +14,6 @@ export interface Props {
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
const [inputRef, setInputFocus] = useFocus();
const suffix =
value !== '' ? (
<Button

View File

@ -59,7 +59,7 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
{...restProps}
style={{
paddingLeft: prefixRect ? prefixRect.width + 12 : undefined,
paddingRight: suffixRect ? suffixRect.width + 12 : undefined,
paddingRight: suffixRect && (suffix || loading) ? suffixRect.width + 12 : undefined,
}}
/>

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;