mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
787e5e78dd
commit
738d5e499e
@ -135,4 +135,71 @@ describe('DateMath', () => {
|
|||||||
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
|
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());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { includes, isDate } from 'lodash';
|
|||||||
import { DateTime, dateTime, dateTimeForTimeZone, ISO_8601, isDateTime, DurationUnit } from './moment_wrapper';
|
import { DateTime, dateTime, dateTimeForTimeZone, ISO_8601, isDateTime, DurationUnit } from './moment_wrapper';
|
||||||
import { TimeZone } from '../types/index';
|
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 {
|
export function isMathString(text: string | DateTime | Date): boolean {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@ -26,7 +26,8 @@ export function isMathString(text: string | DateTime | Date): boolean {
|
|||||||
export function parse(
|
export function parse(
|
||||||
text?: string | DateTime | Date | null,
|
text?: string | DateTime | Date | null,
|
||||||
roundUp?: boolean,
|
roundUp?: boolean,
|
||||||
timezone?: TimeZone
|
timezone?: TimeZone,
|
||||||
|
fiscalYearStartMonth?: number
|
||||||
): DateTime | undefined {
|
): DateTime | undefined {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -67,7 +68,7 @@ export function parse(
|
|||||||
return time;
|
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.
|
* @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`
|
// 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 strippedMathString = mathString.replace(/\s/g, '');
|
||||||
const dateTime = time;
|
const dateTime = time;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@ -107,6 +113,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
|||||||
let type;
|
let type;
|
||||||
let num;
|
let num;
|
||||||
let unit;
|
let unit;
|
||||||
|
let isFiscal = false;
|
||||||
|
|
||||||
if (c === '/') {
|
if (c === '/') {
|
||||||
type = 0;
|
type = 0;
|
||||||
@ -121,7 +128,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
|||||||
if (isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
if (isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
||||||
num = 1;
|
num = 1;
|
||||||
} else if (strippedMathString.length === 2) {
|
} else if (strippedMathString.length === 2) {
|
||||||
num = strippedMathString.charAt(i);
|
num = parseInt(strippedMathString.charAt(i), 10);
|
||||||
} else {
|
} else {
|
||||||
const numFrom = i;
|
const numFrom = i;
|
||||||
while (!isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
while (!isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
||||||
@ -141,15 +148,28 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
|||||||
}
|
}
|
||||||
unit = strippedMathString.charAt(i++);
|
unit = strippedMathString.charAt(i++);
|
||||||
|
|
||||||
|
if (unit === 'f') {
|
||||||
|
unit = strippedMathString.charAt(i++);
|
||||||
|
isFiscal = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!includes(units, unit)) {
|
if (!includes(units, unit)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
if (roundUp) {
|
if (roundUp) {
|
||||||
|
if (isFiscal) {
|
||||||
|
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
|
||||||
|
} else {
|
||||||
dateTime.endOf(unit);
|
dateTime.endOf(unit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isFiscal) {
|
||||||
|
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
|
||||||
} else {
|
} else {
|
||||||
dateTime.startOf(unit);
|
dateTime.startOf(unit);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (type === 1) {
|
} else if (type === 1) {
|
||||||
dateTime.add(num, unit);
|
dateTime.add(num, unit);
|
||||||
} else if (type === 2) {
|
} else if (type === 2) {
|
||||||
@ -159,3 +179,24 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
|||||||
}
|
}
|
||||||
return dateTime;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ export interface DateTimeOptionsWhenParsing extends DateTimeOptions {
|
|||||||
* the returned DateTime value will be 06:00:00.
|
* the returned DateTime value will be 06:00:00.
|
||||||
*/
|
*/
|
||||||
roundUp?: boolean;
|
roundUp?: boolean;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DateTimeParser<T extends DateTimeOptions = DateTimeOptions> = (value: DateTimeInput, options?: T) => DateTime;
|
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;
|
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);
|
return parsed || (moment() as DateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,9 @@ const rangeOptions: TimeOption[] = [
|
|||||||
},
|
},
|
||||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous 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-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/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-5m', to: 'now', display: 'Last 5 minutes' },
|
||||||
{ from: 'now-15m', to: 'now', display: 'Last 15 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-1y', to: 'now', display: 'Last 1 year' },
|
||||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||||
{ from: 'now-5y', to: 'now', display: 'Last 5 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[] = [
|
const hiddenRangeOptions: TimeOption[] = [
|
||||||
@ -192,9 +198,9 @@ export const describeTimeRangeAbbreviation = (range: TimeRange, timeZone?: TimeZ
|
|||||||
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
|
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
|
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone, fiscalYearStartMonth?: number): TimeRange => {
|
||||||
const from = dateTimeParse(raw.from, { roundUp: false, timeZone });
|
const from = dateTimeParse(raw.from, { roundUp: false, timeZone, fiscalYearStartMonth });
|
||||||
const to = dateTimeParse(raw.to, { roundUp: true, timeZone });
|
const to = dateTimeParse(raw.to, { roundUp: true, timeZone, fiscalYearStartMonth });
|
||||||
|
|
||||||
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
|
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
|
||||||
return { from, to, raw };
|
return { from, to, raw };
|
||||||
@ -210,6 +216,15 @@ function isRelativeTime(v: DateTime | string) {
|
|||||||
return false;
|
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 {
|
export function isRelativeTimeRange(raw: RawTimeRange): boolean {
|
||||||
return isRelativeTime(raw.from) || isRelativeTime(raw.to);
|
return isRelativeTime(raw.from) || isRelativeTime(raw.to);
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
|||||||
e2e.components.TimePicker.openButton().click();
|
e2e.components.TimePicker.openButton().click();
|
||||||
|
|
||||||
if (zone) {
|
if (zone) {
|
||||||
e2e().contains('button', 'Change time zone').click();
|
e2e().contains('button', 'Change time settings').click();
|
||||||
|
|
||||||
selectOption({
|
selectOption({
|
||||||
clickToOpen: false,
|
clickToOpen: true,
|
||||||
container: e2e.components.TimeZonePicker.container(),
|
container: e2e.components.TimeZonePicker.container(),
|
||||||
optionText: zone,
|
optionText: zone,
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ import { useStyles2 } from '../../../themes';
|
|||||||
import { Button } from '../../Button';
|
import { Button } from '../../Button';
|
||||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||||
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList';
|
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList';
|
||||||
import { quickOptions } from '../rangeOptions';
|
import { quickOptions } from '../options';
|
||||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
||||||
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';
|
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';
|
||||||
import {
|
import {
|
||||||
|
@ -7,7 +7,7 @@ import { Icon } from '../Icon/Icon';
|
|||||||
import { getInputStyles } from '../Input/Input';
|
import { getInputStyles } from '../Input/Input';
|
||||||
import { TimePickerButtonLabel } from './TimeRangePicker';
|
import { TimePickerButtonLabel } from './TimeRangePicker';
|
||||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||||
import { otherOptions, quickOptions } from './rangeOptions';
|
import { quickOptions } from './options';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { stylesFactory } from '../../themes';
|
import { stylesFactory } from '../../themes';
|
||||||
|
|
||||||
@ -100,7 +100,6 @@ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
|
|||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
value={isValidTimeRange(value) ? (value as TimeRange) : getDefaultTimeRange()}
|
value={isValidTimeRange(value) ? (value as TimeRange) : getDefaultTimeRange()}
|
||||||
onChange={onRangeChange}
|
onChange={onRangeChange}
|
||||||
otherOptions={otherOptions}
|
|
||||||
quickOptions={quickOptions}
|
quickOptions={quickOptions}
|
||||||
onChangeTimeZone={onChangeTimeZone}
|
onChangeTimeZone={onChangeTimeZone}
|
||||||
className={styles.content}
|
className={styles.content}
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
dateMath,
|
dateMath,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Themeable } from '../../types';
|
import { Themeable } from '../../types';
|
||||||
import { otherOptions, quickOptions } from './rangeOptions';
|
import { quickOptions } from './options';
|
||||||
import { ButtonGroup, ToolbarButton } from '../Button';
|
import { ButtonGroup, ToolbarButton } from '../Button';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
@ -32,10 +32,12 @@ export interface TimeRangePickerProps extends Themeable {
|
|||||||
hideText?: boolean;
|
hideText?: boolean;
|
||||||
value: TimeRange;
|
value: TimeRange;
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
timeSyncButton?: JSX.Element;
|
timeSyncButton?: JSX.Element;
|
||||||
isSynced?: boolean;
|
isSynced?: boolean;
|
||||||
onChange: (timeRange: TimeRange) => void;
|
onChange: (timeRange: TimeRange) => void;
|
||||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||||
|
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||||
onMoveBackward: () => void;
|
onMoveBackward: () => void;
|
||||||
onMoveForward: () => void;
|
onMoveForward: () => void;
|
||||||
onZoom: () => void;
|
onZoom: () => void;
|
||||||
@ -75,11 +77,13 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
|||||||
onMoveForward,
|
onMoveForward,
|
||||||
onZoom,
|
onZoom,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
fiscalYearStartMonth,
|
||||||
timeSyncButton,
|
timeSyncButton,
|
||||||
isSynced,
|
isSynced,
|
||||||
theme,
|
theme,
|
||||||
history,
|
history,
|
||||||
onChangeTimeZone,
|
onChangeTimeZone,
|
||||||
|
onChangeFiscalYearStartMonth,
|
||||||
hideQuickRanges,
|
hideQuickRanges,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -117,13 +121,14 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
|||||||
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
|
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
|
||||||
<TimePickerContent
|
<TimePickerContent
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
otherOptions={otherOptions}
|
|
||||||
quickOptions={quickOptions}
|
quickOptions={quickOptions}
|
||||||
history={history}
|
history={history}
|
||||||
showHistory
|
showHistory
|
||||||
onChangeTimeZone={onChangeTimeZone}
|
onChangeTimeZone={onChangeTimeZone}
|
||||||
|
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||||
hideQuickRanges={hideQuickRanges}
|
hideQuickRanges={hideQuickRanges}
|
||||||
/>
|
/>
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
|
@ -38,14 +38,12 @@ describe('TimePickerContent', () => {
|
|||||||
|
|
||||||
it('renders with relative picker', () => {
|
it('renders with relative picker', () => {
|
||||||
renderComponent({ value: absoluteValue });
|
renderComponent({ value: absoluteValue });
|
||||||
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
|
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders without relative picker', () => {
|
it('renders without relative picker', () => {
|
||||||
renderComponent({ value: absoluteValue, hideQuickRanges: true });
|
renderComponent({ value: absoluteValue, hideQuickRanges: true });
|
||||||
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with timezone picker', () => {
|
it('renders with timezone picker', () => {
|
||||||
@ -86,14 +84,12 @@ describe('TimePickerContent', () => {
|
|||||||
|
|
||||||
it('renders with relative picker', () => {
|
it('renders with relative picker', () => {
|
||||||
renderComponent({ value: absoluteValue, isFullscreen: false });
|
renderComponent({ value: absoluteValue, isFullscreen: false });
|
||||||
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
|
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders without relative picker', () => {
|
it('renders without relative picker', () => {
|
||||||
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
|
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
|
||||||
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
|
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
|
||||||
@ -139,6 +135,10 @@ function renderComponent({
|
|||||||
<TimePickerContentWithScreenSize
|
<TimePickerContentWithScreenSize
|
||||||
onChangeTimeZone={noop}
|
onChangeTimeZone={noop}
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
|
quickOptions={[
|
||||||
|
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||||
|
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
|
||||||
|
]}
|
||||||
timeZone="utc"
|
timeZone="utc"
|
||||||
value={value}
|
value={value}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
|
@ -12,6 +12,7 @@ import { TimeRangeList } from './TimeRangeList';
|
|||||||
import { TimePickerFooter } from './TimePickerFooter';
|
import { TimePickerFooter } from './TimePickerFooter';
|
||||||
import { getFocusStyles } from '../../../themes/mixins';
|
import { getFocusStyles } from '../../../themes/mixins';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { FilterInput } from '../..';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
|
||||||
return {
|
return {
|
||||||
@ -46,11 +47,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRang
|
|||||||
rightSide: css`
|
rightSide: css`
|
||||||
width: 40% !important;
|
width: 40% !important;
|
||||||
border-right: ${isReversed ? `1px solid ${theme.colors.border.weak}` : 'none'};
|
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) {
|
@media only screen and (max-width: ${theme.breakpoints.values.lg}px) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
timeRangeFilter: css`
|
||||||
|
padding: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
spacing: css`
|
spacing: css`
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
`,
|
`,
|
||||||
@ -127,9 +132,10 @@ interface Props {
|
|||||||
value: TimeRange;
|
value: TimeRange;
|
||||||
onChange: (timeRange: TimeRange) => void;
|
onChange: (timeRange: TimeRange) => void;
|
||||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||||
|
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
quickOptions?: TimeOption[];
|
quickOptions?: TimeOption[];
|
||||||
otherOptions?: TimeOption[];
|
|
||||||
history?: TimeRange[];
|
history?: TimeRange[];
|
||||||
showHistory?: boolean;
|
showHistory?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -150,11 +156,11 @@ interface FormProps extends Omit<Props, 'history'> {
|
|||||||
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (props) => {
|
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (props) => {
|
||||||
const {
|
const {
|
||||||
quickOptions = [],
|
quickOptions = [],
|
||||||
otherOptions = [],
|
|
||||||
isReversed,
|
isReversed,
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
hideQuickRanges,
|
hideQuickRanges,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
fiscalYearStartMonth,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
history,
|
history,
|
||||||
@ -162,6 +168,7 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
|||||||
className,
|
className,
|
||||||
hideTimeZone,
|
hideTimeZone,
|
||||||
onChangeTimeZone,
|
onChangeTimeZone,
|
||||||
|
onChangeFiscalYearStartMonth,
|
||||||
} = props;
|
} = props;
|
||||||
const isHistoryEmpty = !history?.length;
|
const isHistoryEmpty = !history?.length;
|
||||||
const isContainerTall =
|
const isContainerTall =
|
||||||
@ -169,7 +176,10 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
|
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
|
||||||
const historyOptions = mapToHistoryOptions(history, timeZone);
|
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) => {
|
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||||
return onChange(mapOptionToTimeRange(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 id="TimePickerContent" className={cx(styles.container, className)}>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{(!isFullscreen || !hideQuickRanges) && (
|
{(!isFullscreen || !hideQuickRanges) && (
|
||||||
<CustomScrollbar className={styles.rightSide}>
|
<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} />}
|
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
||||||
{!hideQuickRanges && (
|
{!hideQuickRanges && (
|
||||||
<>
|
<TimeRangeList options={filteredQuickOptions} onChange={onChangeTimeOption} value={timeOption} />
|
||||||
<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>
|
</CustomScrollbar>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isFullscreen && (
|
{isFullscreen && (
|
||||||
<div className={styles.leftSide}>
|
<div className={styles.leftSide}>
|
||||||
@ -206,7 +213,14 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
|
{!hideTimeZone && isFullscreen && (
|
||||||
|
<TimePickerFooter
|
||||||
|
timeZone={timeZone}
|
||||||
|
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||||
|
onChangeTimeZone={onChangeTimeZone}
|
||||||
|
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -268,7 +282,7 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FullScreenForm: 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 theme = useTheme2();
|
||||||
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
|
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
|
||||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||||
@ -282,18 +296,19 @@ const FullScreenForm: React.FC<FormProps> = (props) => {
|
|||||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||||
</div>
|
</div>
|
||||||
<TimeRangeForm
|
<TimeRangeForm
|
||||||
value={props.value}
|
value={value}
|
||||||
timeZone={props.timeZone}
|
timeZone={timeZone}
|
||||||
onApply={props.onChange}
|
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||||
|
onApply={onChange}
|
||||||
isFullscreen={true}
|
isFullscreen={true}
|
||||||
isReversed={props.isReversed}
|
isReversed={isReversed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{props.showHistory && (
|
{props.showHistory && (
|
||||||
<div className={styles.recent}>
|
<div className={styles.recent}>
|
||||||
<TimeRangeList
|
<TimeRangeList
|
||||||
title="Recently used absolute ranges"
|
title="Recently used absolute ranges"
|
||||||
options={props.historyOptions || []}
|
options={historyOptions || []}
|
||||||
onChange={onChangeTimeOption}
|
onChange={onChangeTimeOption}
|
||||||
placeholderEmpty={<EmptyRecentList />}
|
placeholderEmpty={<EmptyRecentList />}
|
||||||
/>
|
/>
|
||||||
@ -338,23 +353,13 @@ function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOpt
|
|||||||
|
|
||||||
EmptyRecentList.displayName = 'EmptyRecentList';
|
EmptyRecentList.displayName = 'EmptyRecentList';
|
||||||
|
|
||||||
const useTimeOption = (
|
const useTimeOption = (raw: RawTimeRange, quickOptions: TimeOption[]): TimeOption | undefined => {
|
||||||
raw: RawTimeRange,
|
|
||||||
quickOptions: TimeOption[],
|
|
||||||
otherOptions: TimeOption[]
|
|
||||||
): TimeOption | undefined => {
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!rangeUtil.isRelativeTimeRange(raw)) {
|
if (!rangeUtil.isRelativeTimeRange(raw)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const quickOption = quickOptions.find((option) => {
|
return quickOptions.find((option) => {
|
||||||
return option.from === raw.from && option.to === raw.to;
|
return option.from === raw.from && option.to === raw.to;
|
||||||
});
|
});
|
||||||
if (quickOption) {
|
}, [raw, quickOptions]);
|
||||||
return quickOption;
|
|
||||||
}
|
|
||||||
return otherOptions.find((option) => {
|
|
||||||
return option.from === raw.from && option.to === raw.to;
|
|
||||||
});
|
|
||||||
}, [raw, otherOptions, quickOptions]);
|
|
||||||
};
|
};
|
||||||
|
@ -9,18 +9,29 @@ import { Button } from '../../Button';
|
|||||||
import { TimeZonePicker } from '../TimeZonePicker';
|
import { TimeZonePicker } from '../TimeZonePicker';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { Field, RadioButtonGroup, Select } from '../..';
|
||||||
|
import { monthOptions } from '../options';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||||
|
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimePickerFooter: FC<Props> = (props) => {
|
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 [isEditing, setEditing] = useState(false);
|
||||||
|
const [editMode, setEditMode] = useState('tz');
|
||||||
|
|
||||||
const onToggleChangeTz = useCallback(
|
const onToggleChangeTimeSettings = useCallback(
|
||||||
(event?: React.MouseEvent) => {
|
(event?: React.MouseEvent) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -43,28 +54,8 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
|||||||
return null;
|
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 (
|
return (
|
||||||
|
<div>
|
||||||
<section aria-label="Time zone selection" className={style.container}>
|
<section aria-label="Time zone selection" className={style.container}>
|
||||||
<div className={style.timeZoneContainer}>
|
<div className={style.timeZoneContainer}>
|
||||||
<div className={style.timeZone}>
|
<div className={style.timeZone}>
|
||||||
@ -75,10 +66,60 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
|||||||
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
|
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
|
||||||
</div>
|
</div>
|
||||||
<div className={style.spacer} />
|
<div className={style.spacer} />
|
||||||
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
|
<Button variant="secondary" onClick={onToggleChangeTimeSettings} size="sm">
|
||||||
Change time zone
|
Change time settings
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</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;
|
align-items: center;
|
||||||
`,
|
`,
|
||||||
editContainer: css`
|
editContainer: css`
|
||||||
|
border-top: 1px solid ${theme.colors.border.weak};
|
||||||
|
padding: 11px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
`,
|
`,
|
||||||
spacer: css`
|
spacer: css`
|
||||||
margin-left: 7px;
|
margin-left: 7px;
|
||||||
`,
|
`,
|
||||||
|
timeSettingContainer: css`
|
||||||
|
padding-top: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
fiscalYearField: css`
|
||||||
|
margin-bottom: 0px;
|
||||||
|
`,
|
||||||
timeZoneContainer: css`
|
timeZoneContainer: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import {
|
import {
|
||||||
dateMath,
|
dateMath,
|
||||||
DateTime,
|
DateTime,
|
||||||
dateTimeFormat,
|
dateTimeFormat,
|
||||||
dateTimeParse,
|
dateTimeParse,
|
||||||
|
GrafanaTheme2,
|
||||||
isDateTime,
|
isDateTime,
|
||||||
rangeUtil,
|
rangeUtil,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
@ -11,6 +13,8 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
|
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Icon, Tooltip } from '../..';
|
||||||
|
import { useStyles2 } from '../../..';
|
||||||
import { Button } from '../../Button';
|
import { Button } from '../../Button';
|
||||||
import { Field } from '../../Forms/Field';
|
import { Field } from '../../Forms/Field';
|
||||||
import { Input } from '../../Input/Input';
|
import { Input } from '../../Input/Input';
|
||||||
@ -21,6 +25,7 @@ interface Props {
|
|||||||
value: TimeRange;
|
value: TimeRange;
|
||||||
onApply: (range: TimeRange) => void;
|
onApply: (range: TimeRange) => void;
|
||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
roundup?: boolean;
|
roundup?: boolean;
|
||||||
isReversed?: boolean;
|
isReversed?: boolean;
|
||||||
}
|
}
|
||||||
@ -37,8 +42,9 @@ const ERROR_MESSAGES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TimeRangeForm: React.FC<Props> = (props) => {
|
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 [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone);
|
||||||
|
const style = useStyles2(getStyles);
|
||||||
|
|
||||||
const [from, setFrom] = useState<InputState>(fromValue);
|
const [from, setFrom] = useState<InputState>(fromValue);
|
||||||
const [to, setTo] = useState<InputState>(toValue);
|
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 raw: RawTimeRange = { from: from.value, to: to.value };
|
||||||
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
|
const timeRange = rangeUtil.convertRawToRange(raw, timeZone, fiscalYearStartMonth);
|
||||||
|
|
||||||
onApplyFromProps(timeRange);
|
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(
|
const onChange = useCallback(
|
||||||
@ -93,11 +99,24 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
[timeZone]
|
[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} />;
|
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-label="Absolute time ranges">
|
<div aria-label="Absolute time ranges">
|
||||||
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
||||||
|
<div className={style.fieldContainer}>
|
||||||
<Input
|
<Input
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
@ -106,8 +125,11 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
aria-label={selectors.components.TimePicker.fromField}
|
aria-label={selectors.components.TimePicker.fromField}
|
||||||
value={from.value}
|
value={from.value}
|
||||||
/>
|
/>
|
||||||
|
{fyTooltip}
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="To" invalid={to.invalid} error={to.errorMessage}>
|
<Field label="To" invalid={to.invalid} error={to.errorMessage}>
|
||||||
|
<div className={style.fieldContainer}>
|
||||||
<Input
|
<Input
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
@ -116,6 +138,8 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
aria-label={selectors.components.TimePicker.toField}
|
aria-label={selectors.components.TimePicker.toField}
|
||||||
value={to.value}
|
value={to.value}
|
||||||
/>
|
/>
|
||||||
|
{fyTooltip}
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Button data-testid={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
|
<Button data-testid={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
|
||||||
Apply time range
|
Apply time range
|
||||||
@ -185,3 +209,15 @@ function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean
|
|||||||
const parsed = dateTimeParse(value, { roundUp, timeZone });
|
const parsed = dateTimeParse(value, { roundUp, timeZone });
|
||||||
return parsed.isValid();
|
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)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -26,7 +26,7 @@ const getOptionsStyles = stylesFactory(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title?: string;
|
||||||
options: TimeOption[];
|
options: TimeOption[];
|
||||||
value?: TimeOption;
|
value?: TimeOption;
|
||||||
onChange: (option: TimeOption) => void;
|
onChange: (option: TimeOption) => void;
|
||||||
@ -69,7 +69,7 @@ const Options: React.FC<Props> = ({ options, value, onChange, title }) => {
|
|||||||
value={option}
|
value={option}
|
||||||
selected={isEqual(option, value)}
|
selected={isEqual(option, value)}
|
||||||
onSelect={onChange}
|
onSelect={onChange}
|
||||||
name={title}
|
name={title ?? 'Time ranges'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -43,7 +43,6 @@ export const TimeZonePicker: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
|
||||||
value={selected}
|
value={selected}
|
||||||
placeholder="Type to search (country, city, abbreviation)"
|
placeholder="Type to search (country, city, abbreviation)"
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TimeOption } from '@grafana/data';
|
import { SelectableValue, TimeOption } from '@grafana/data';
|
||||||
|
|
||||||
export const quickOptions: TimeOption[] = [
|
export const quickOptions: TimeOption[] = [
|
||||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
{ 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-1y', to: 'now', display: 'Last 1 year' },
|
||||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||||
{ from: 'now-5y', to: 'now', display: 'Last 5 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-1d/d', to: 'now-1d/d', display: 'Yesterday' },
|
||||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before 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-7d/d', to: 'now-7d/d', display: 'This day last week' },
|
||||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous 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-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/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/d', display: 'Today' },
|
||||||
{ from: 'now/d', to: 'now', display: 'Today so far' },
|
{ from: 'now/d', to: 'now', display: 'Today so far' },
|
||||||
{ from: 'now/w', to: 'now/w', display: 'This week' },
|
{ 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/M', to: 'now', display: 'This month so far' },
|
||||||
{ from: 'now/y', to: 'now/y', display: 'This year' },
|
{ from: 'now/y', to: 'now/y', display: 'This year' },
|
||||||
{ from: 'now/y', to: 'now', display: 'This year so far' },
|
{ 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 },
|
||||||
];
|
];
|
@ -14,7 +14,6 @@ export interface Props {
|
|||||||
|
|
||||||
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
|
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
|
||||||
const [inputRef, setInputFocus] = useFocus();
|
const [inputRef, setInputFocus] = useFocus();
|
||||||
|
|
||||||
const suffix =
|
const suffix =
|
||||||
value !== '' ? (
|
value !== '' ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -59,7 +59,7 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
|||||||
{...restProps}
|
{...restProps}
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: prefixRect ? prefixRect.width + 12 : undefined,
|
paddingLeft: prefixRect ? prefixRect.width + 12 : undefined,
|
||||||
paddingRight: suffixRect ? suffixRect.width + 12 : undefined,
|
paddingRight: suffixRect && (suffix || loading) ? suffixRect.width + 12 : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export class User {
|
|||||||
login: string;
|
login: string;
|
||||||
orgCount: number;
|
orgCount: number;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
fiscalYearStartMonth: number;
|
||||||
helpFlags1: number;
|
helpFlags1: number;
|
||||||
lightTheme: boolean;
|
lightTheme: boolean;
|
||||||
hasEditPermissionInFolders: boolean;
|
hasEditPermissionInFolders: boolean;
|
||||||
@ -30,6 +31,7 @@ export class User {
|
|||||||
this.login = '';
|
this.login = '';
|
||||||
this.orgCount = 0;
|
this.orgCount = 0;
|
||||||
this.timezone = '';
|
this.timezone = '';
|
||||||
|
this.fiscalYearStartMonth = 0;
|
||||||
this.helpFlags1 = 0;
|
this.helpFlags1 = 0;
|
||||||
this.lightTheme = false;
|
this.lightTheme = false;
|
||||||
this.hasEditPermissionInFolders = false;
|
this.hasEditPermissionInFolders = false;
|
||||||
|
@ -302,7 +302,7 @@ describe('getTimeRangeFromUrl', () => {
|
|||||||
it('should parse moment date', () => {
|
it('should parse moment date', () => {
|
||||||
// convert date strings to moment object
|
// 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 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);
|
expect(result.raw).toEqual(range);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -311,7 +311,7 @@ describe('getTimeRangeFromUrl', () => {
|
|||||||
from: dateTime('2020-10-22T10:00:00Z').valueOf().toString(),
|
from: dateTime('2020-10-22T10:00:00Z').valueOf().toString(),
|
||||||
to: dateTime('2020-10-22T11: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.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
|
||||||
expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11: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());
|
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(),
|
from: dateTime('2020-10-22T10:00:00Z').toISOString(),
|
||||||
to: dateTime('2020-10-22T11: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.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
|
||||||
expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11: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());
|
expect(result.raw.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf());
|
||||||
|
@ -341,10 +341,10 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSour
|
|||||||
return queryKeys;
|
return queryKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => {
|
export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange, fiscalYearStartMonth: number): TimeRange => {
|
||||||
return {
|
return {
|
||||||
from: dateMath.parse(rawRange.from, false, timeZone as any)!,
|
from: dateMath.parse(rawRange.from, false, timeZone as any, fiscalYearStartMonth)!,
|
||||||
to: dateMath.parse(rawRange.to, true, timeZone as any)!,
|
to: dateMath.parse(rawRange.to, true, timeZone as any, fiscalYearStartMonth)!,
|
||||||
raw: rawRange,
|
raw: rawRange,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -387,7 +387,11 @@ const parseRawTime = (value: string | DateTime): TimeFragment | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => {
|
export const getTimeRangeFromUrl = (
|
||||||
|
range: RawTimeRange,
|
||||||
|
timeZone: TimeZone,
|
||||||
|
fiscalYearStartMonth: number
|
||||||
|
): TimeRange => {
|
||||||
const raw = {
|
const raw = {
|
||||||
from: parseRawTime(range.from)!,
|
from: parseRawTime(range.from)!,
|
||||||
to: parseRawTime(range.to)!,
|
to: parseRawTime(range.to)!,
|
||||||
|
@ -70,6 +70,11 @@ export class DashNavTimeControls extends Component<Props> {
|
|||||||
this.onRefresh();
|
this.onRefresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onChangeFiscalYearStartMonth = (month: number) => {
|
||||||
|
this.props.dashboard.fiscalYearStartMonth = month;
|
||||||
|
this.onRefresh();
|
||||||
|
};
|
||||||
|
|
||||||
onZoom = () => {
|
onZoom = () => {
|
||||||
appEvents.publish(new ZoomOutEvent(2));
|
appEvents.publish(new ZoomOutEvent(2));
|
||||||
};
|
};
|
||||||
@ -81,6 +86,7 @@ export class DashNavTimeControls extends Component<Props> {
|
|||||||
|
|
||||||
const timePickerValue = getTimeSrv().timeRange();
|
const timePickerValue = getTimeSrv().timeRange();
|
||||||
const timeZone = dashboard.getTimezone();
|
const timeZone = dashboard.getTimezone();
|
||||||
|
const fiscalYearStartMonth = dashboard.fiscalYearStartMonth;
|
||||||
const hideIntervalPicker = dashboard.panelInEdit?.isEditing;
|
const hideIntervalPicker = dashboard.panelInEdit?.isEditing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -89,10 +95,12 @@ export class DashNavTimeControls extends Component<Props> {
|
|||||||
value={timePickerValue}
|
value={timePickerValue}
|
||||||
onChange={this.onChangeTimePicker}
|
onChange={this.onChangeTimePicker}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||||
onMoveBackward={this.onMoveBack}
|
onMoveBackward={this.onMoveBack}
|
||||||
onMoveForward={this.onMoveForward}
|
onMoveForward={this.onMoveForward}
|
||||||
onZoom={this.onZoom}
|
onZoom={this.onZoom}
|
||||||
onChangeTimeZone={this.onChangeTimeZone}
|
onChangeTimeZone={this.onChangeTimeZone}
|
||||||
|
onChangeFiscalYearStartMonth={this.onChangeFiscalYearStartMonth}
|
||||||
/>
|
/>
|
||||||
<RefreshPicker
|
<RefreshPicker
|
||||||
onIntervalChanged={this.onChangeRefreshInterval}
|
onIntervalChanged={this.onChangeRefreshInterval}
|
||||||
|
@ -60,7 +60,11 @@ export class TimeSrv {
|
|||||||
// remember time at load so we can go back to it
|
// remember time at load so we can go back to it
|
||||||
this.timeAtLoad = cloneDeep(this.time);
|
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)) {
|
if (range.to.isBefore(range.from)) {
|
||||||
this.setTime(
|
this.setTime(
|
||||||
@ -327,8 +331,8 @@ export class TimeSrv {
|
|||||||
const timezone = this.dashboard ? this.dashboard.getTimezone() : undefined;
|
const timezone = this.dashboard ? this.dashboard.getTimezone() : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: dateMath.parse(raw.from, false, timezone)!,
|
from: dateMath.parse(raw.from, false, timezone, this.dashboard?.fiscalYearStartMonth)!,
|
||||||
to: dateMath.parse(raw.to, true, timezone)!,
|
to: dateMath.parse(raw.to, true, timezone, this.dashboard?.fiscalYearStartMonth)!,
|
||||||
raw: raw,
|
raw: raw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ export class DashboardModel {
|
|||||||
panels: PanelModel[];
|
panels: PanelModel[];
|
||||||
panelInEdit?: PanelModel;
|
panelInEdit?: PanelModel;
|
||||||
panelInView?: PanelModel;
|
panelInView?: PanelModel;
|
||||||
|
fiscalYearStartMonth?: number;
|
||||||
private hasChangesThatAffectsAllPanels: boolean;
|
private hasChangesThatAffectsAllPanels: boolean;
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
@ -131,26 +132,27 @@ export class DashboardModel {
|
|||||||
this.id = data.id || null;
|
this.id = data.id || null;
|
||||||
this.uid = data.uid || null;
|
this.uid = data.uid || null;
|
||||||
this.revision = data.revision;
|
this.revision = data.revision;
|
||||||
this.title = data.title || 'No Title';
|
this.title = data.title ?? 'No Title';
|
||||||
this.autoUpdate = data.autoUpdate;
|
this.autoUpdate = data.autoUpdate;
|
||||||
this.description = data.description;
|
this.description = data.description;
|
||||||
this.tags = data.tags || [];
|
this.tags = data.tags ?? [];
|
||||||
this.style = data.style || 'dark';
|
this.style = data.style ?? 'dark';
|
||||||
this.timezone = data.timezone || '';
|
this.timezone = data.timezone ?? '';
|
||||||
this.editable = data.editable !== false;
|
this.editable = data.editable !== false;
|
||||||
this.graphTooltip = data.graphTooltip || 0;
|
this.graphTooltip = data.graphTooltip || 0;
|
||||||
this.time = data.time || { from: 'now-6h', to: 'now' };
|
this.time = data.time ?? { from: 'now-6h', to: 'now' };
|
||||||
this.timepicker = data.timepicker || {};
|
this.timepicker = data.timepicker ?? {};
|
||||||
this.liveNow = Boolean(data.liveNow);
|
this.liveNow = Boolean(data.liveNow);
|
||||||
this.templating = this.ensureListExist(data.templating);
|
this.templating = this.ensureListExist(data.templating);
|
||||||
this.annotations = this.ensureListExist(data.annotations);
|
this.annotations = this.ensureListExist(data.annotations);
|
||||||
this.refresh = data.refresh;
|
this.refresh = data.refresh;
|
||||||
this.snapshot = data.snapshot;
|
this.snapshot = data.snapshot;
|
||||||
this.schemaVersion = data.schemaVersion || 0;
|
this.schemaVersion = data.schemaVersion ?? 0;
|
||||||
this.version = data.version || 0;
|
this.fiscalYearStartMonth = data.fiscalYearStartMonth ?? 0;
|
||||||
this.links = data.links || [];
|
this.version = data.version ?? 0;
|
||||||
|
this.links = data.links ?? [];
|
||||||
this.gnetId = data.gnetId || null;
|
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.formatDate = this.formatDate.bind(this);
|
||||||
|
|
||||||
this.resetOriginalVariables(true);
|
this.resetOriginalVariables(true);
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
lastUsedDatasourceKeyForOrgId,
|
lastUsedDatasourceKeyForOrgId,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors';
|
||||||
import Explore from './Explore';
|
import Explore from './Explore';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
@ -99,13 +99,14 @@ const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
|
|||||||
function mapStateToProps(state: StoreState, props: OwnProps) {
|
function mapStateToProps(state: StoreState, props: OwnProps) {
|
||||||
const urlState = parseUrlState(props.urlQuery);
|
const urlState = parseUrlState(props.urlQuery);
|
||||||
const timeZone = getTimeZone(state.user);
|
const timeZone = getTimeZone(state.user);
|
||||||
|
const fiscalYearStartMonth = getFiscalYearStartMonth(state.user);
|
||||||
|
|
||||||
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
|
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
|
||||||
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
||||||
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
||||||
const initialRange = urlRange
|
const initialRange = urlRange
|
||||||
? getTimeRangeFromUrlMemoized(urlRange, timeZone)
|
? getTimeRangeFromUrlMemoized(urlRange, timeZone, fiscalYearStartMonth)
|
||||||
: getTimeRange(timeZone, DEFAULT_RANGE);
|
: getTimeRange(timeZone, DEFAULT_RANGE, fiscalYearStartMonth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialized: state.explore[props.exploreId]?.initialized,
|
initialized: state.explore[props.exploreId]?.initialized,
|
||||||
|
@ -19,11 +19,13 @@ export interface Props {
|
|||||||
hideText?: boolean;
|
hideText?: boolean;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
fiscalYearStartMonth: number;
|
||||||
splitted: boolean;
|
splitted: boolean;
|
||||||
syncedTimes: boolean;
|
syncedTimes: boolean;
|
||||||
onChangeTimeSync: () => void;
|
onChangeTimeSync: () => void;
|
||||||
onChangeTime: (range: RawTimeRange) => void;
|
onChangeTime: (range: RawTimeRange) => void;
|
||||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||||
|
onChangeFiscalYearStartMonth: (fiscalYearStartMonth: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExploreTimeControls extends Component<Props> {
|
export class ExploreTimeControls extends Component<Props> {
|
||||||
@ -63,11 +65,22 @@ export class ExploreTimeControls extends Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
|
||||||
const timePickerCommonProps = {
|
const timePickerCommonProps = {
|
||||||
value: range,
|
value: range,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
fiscalYearStartMonth,
|
||||||
onMoveBackward: this.onMoveBack,
|
onMoveBackward: this.onMoveBack,
|
||||||
onMoveForward: this.onMoveForward,
|
onMoveForward: this.onMoveForward,
|
||||||
onZoom: this.onZoom,
|
onZoom: this.onZoom,
|
||||||
@ -81,6 +94,7 @@ export class ExploreTimeControls extends Component<Props> {
|
|||||||
isSynced={syncedTimes}
|
isSynced={syncedTimes}
|
||||||
onChange={this.onChangeTimePicker}
|
onChange={this.onChangeTimePicker}
|
||||||
onChangeTimeZone={onChangeTimeZone}
|
onChangeTimeZone={onChangeTimeZone}
|
||||||
|
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,8 @@ import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
|||||||
import { changeDatasource } from './state/datasource';
|
import { changeDatasource } from './state/datasource';
|
||||||
import { splitClose, splitOpen } from './state/main';
|
import { splitClose, splitOpen } from './state/main';
|
||||||
import { syncTimes, changeRefreshInterval } from './state/time';
|
import { syncTimes, changeRefreshInterval } from './state/time';
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors';
|
||||||
import { updateTimeZoneForSession } from '../profile/state/reducers';
|
import { updateFiscalYearStartMonthForSession, updateTimeZoneForSession } from '../profile/state/reducers';
|
||||||
import { ExploreTimeControls } from './ExploreTimeControls';
|
import { ExploreTimeControls } from './ExploreTimeControls';
|
||||||
import { LiveTailButton } from './LiveTailButton';
|
import { LiveTailButton } from './LiveTailButton';
|
||||||
import { RunButton } from './RunButton';
|
import { RunButton } from './RunButton';
|
||||||
@ -65,6 +65,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
loading,
|
loading,
|
||||||
range,
|
range,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
fiscalYearStartMonth,
|
||||||
splitted,
|
splitted,
|
||||||
syncedTimes,
|
syncedTimes,
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
@ -75,6 +76,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
isPaused,
|
isPaused,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
onChangeTimeZone,
|
onChangeTimeZone,
|
||||||
|
onChangeFiscalYearStartMonth,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
||||||
@ -158,12 +160,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
exploreId={exploreId}
|
exploreId={exploreId}
|
||||||
range={range}
|
range={range}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||||
onChangeTime={onChangeTime}
|
onChangeTime={onChangeTime}
|
||||||
splitted={splitted}
|
splitted={splitted}
|
||||||
syncedTimes={syncedTimes}
|
syncedTimes={syncedTimes}
|
||||||
onChangeTimeSync={this.onChangeTimeSync}
|
onChangeTimeSync={this.onChangeTimeSync}
|
||||||
hideText={showSmallTimePicker}
|
hideText={showSmallTimePicker}
|
||||||
onChangeTimeZone={onChangeTimeZone}
|
onChangeTimeZone={onChangeTimeZone}
|
||||||
|
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -230,6 +234,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
|
|||||||
loading,
|
loading,
|
||||||
range,
|
range,
|
||||||
timeZone: getTimeZone(state.user),
|
timeZone: getTimeZone(state.user),
|
||||||
|
fiscalYearStartMonth: getFiscalYearStartMonth(state.user),
|
||||||
splitted: isSplit(state),
|
splitted: isSplit(state),
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
hasLiveOption,
|
hasLiveOption,
|
||||||
@ -250,6 +255,7 @@ const mapDispatchToProps = {
|
|||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
syncTimes,
|
syncTimes,
|
||||||
onChangeTimeZone: updateTimeZoneForSession,
|
onChangeTimeZone: updateTimeZoneForSession,
|
||||||
|
onChangeFiscalYearStartMonth: updateFiscalYearStartMonthForSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
@ -24,7 +24,7 @@ import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
|
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
|
||||||
// Types
|
// Types
|
||||||
import { ThunkResult } from 'app/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 { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { getRichHistory } from '../../../core/utils/richHistory';
|
import { getRichHistory } from '../../../core/utils/richHistory';
|
||||||
import { richHistoryUpdatedAction } from './main';
|
import { richHistoryUpdatedAction } from './main';
|
||||||
@ -153,7 +153,8 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeZone = getTimeZone(getState().user);
|
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
|
// commit changes based on the diff of new url vs old url
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import { RefreshPicker } from '@grafana/ui';
|
|||||||
import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore';
|
import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore';
|
||||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
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 { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { runQueries } from './query';
|
import { runQueries } from './query';
|
||||||
@ -78,6 +78,7 @@ export const updateTime = (config: {
|
|||||||
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
|
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
|
||||||
const itemState = getState().explore[exploreId]!;
|
const itemState = getState().explore[exploreId]!;
|
||||||
const timeZone = getTimeZone(getState().user);
|
const timeZone = getTimeZone(getState().user);
|
||||||
|
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
|
||||||
const { range: rangeInState } = itemState;
|
const { range: rangeInState } = itemState;
|
||||||
let rawRange: RawTimeRange = rangeInState.raw;
|
let rawRange: RawTimeRange = rangeInState.raw;
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ export const updateTime = (config: {
|
|||||||
rawRange = actionRange;
|
rawRange = actionRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = getTimeRange(timeZone, rawRange);
|
const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth);
|
||||||
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
|
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
|
||||||
|
|
||||||
getTimeSrv().init(
|
getTimeSrv().init(
|
||||||
|
@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core';
|
|||||||
export interface UserState {
|
export interface UserState {
|
||||||
orgId: number;
|
orgId: number;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
fiscalYearStartMonth: number;
|
||||||
user: UserDTO | null;
|
user: UserDTO | null;
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
orgs: UserOrg[];
|
orgs: UserOrg[];
|
||||||
@ -22,6 +23,7 @@ export interface UserState {
|
|||||||
export const initialUserState: UserState = {
|
export const initialUserState: UserState = {
|
||||||
orgId: config.bootData.user.orgId,
|
orgId: config.bootData.user.orgId,
|
||||||
timeZone: config.bootData.user.timezone,
|
timeZone: config.bootData.user.timezone,
|
||||||
|
fiscalYearStartMonth: 0,
|
||||||
orgsAreLoading: false,
|
orgsAreLoading: false,
|
||||||
sessionsAreLoading: false,
|
sessionsAreLoading: false,
|
||||||
teamsAreLoading: false,
|
teamsAreLoading: false,
|
||||||
@ -39,6 +41,9 @@ export const slice = createSlice({
|
|||||||
updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => {
|
updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => {
|
||||||
state.timeZone = action.payload.timeZone;
|
state.timeZone = action.payload.timeZone;
|
||||||
},
|
},
|
||||||
|
updateFiscalYearStartMonth: (state, action: PayloadAction<{ fiscalYearStartMonth: number }>) => {
|
||||||
|
state.fiscalYearStartMonth = action.payload.fiscalYearStartMonth;
|
||||||
|
},
|
||||||
setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => {
|
setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => {
|
||||||
state.isUpdating = action.payload.updating;
|
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> => {
|
export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void> => {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
if (!isString(timeZone) || isEmpty(timeZone)) {
|
if (!isString(timeZone) || isEmpty(timeZone)) {
|
||||||
@ -109,6 +121,7 @@ export const {
|
|||||||
initLoadSessions,
|
initLoadSessions,
|
||||||
sessionsLoaded,
|
sessionsLoaded,
|
||||||
updateTimeZone,
|
updateTimeZone,
|
||||||
|
updateFiscalYearStartMonth,
|
||||||
} = slice.actions;
|
} = slice.actions;
|
||||||
|
|
||||||
export const userReducer = slice.reducer;
|
export const userReducer = slice.reducer;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
import { UserState } from './reducers';
|
import { UserState } from './reducers';
|
||||||
|
|
||||||
export const getTimeZone = (state: UserState) => state.timeZone;
|
export const getTimeZone = (state: UserState) => state.timeZone;
|
||||||
|
export const getFiscalYearStartMonth = (state: UserState) => state.fiscalYearStartMonth;
|
||||||
|
Loading…
Reference in New Issue
Block a user