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());
|
||||
});
|
||||
});
|
||||
|
||||
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 { 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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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 },
|
||||
];
|
@ -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
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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)!,
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { UserState } from './reducers';
|
||||
|
||||
export const getTimeZone = (state: UserState) => state.timeZone;
|
||||
export const getFiscalYearStartMonth = (state: UserState) => state.fiscalYearStartMonth;
|
||||
|
Loading…
Reference in New Issue
Block a user