mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: new updated time picker (#20931)
* Dashboard: started to implement new time picker. * TimePicker: working in full screen (except calendar). * TimePicker: first draft on narrow screen variant. * TimePicker: small adjustments to the narrow design. * TimePicker: enabled range selection and started to style calendar. * TimePicker: applied some more styling. * Calendar: added so the calendar range selection is styled properly. * Calendar: added styling for timepicker calendar in narrow screen. * TimePicker: made it possible to select range from calendar. * TimePicker: made the calendar have previous selected value. * TimePicker: moved calendar to be able to update form state. * TimePicker: calendar is now displayed onFocus or onClick. * TimePicker: calendar will be closed if click outside input. * Calendar: fixed the styling of the calendar in narrow screen. * Calendar: made it work properly with narrow screen. * TimePicker: connected recent to absolute time range. * TimePicker: changed the label on recent ranges. * TimePicker: cleaned up the range list and options. * TimePicker: some more cleaning up. * TimePicker: cleaned up the calendar a bit. * TimePicker: some more refactorings. * TimePicker: refactorings. * TimePicker: styled modal properly. * TimePicker: empty recent list. * TimePicker: width when calendar in full screen. * TimePicker: will validate input value. * TimePicker: removed unused code. * TimePicker: positioning with emotion instead of sass. * Calendar: Made sure we send the dates in the correct order to the calendar. * TimePicker: fixed theme. * TimePicker: fixed positioning of the content. * TimePicker: positioning of narrow. * TimePicker: added some simple tets. * TimePicker: fixed issue with invalid and added error message. * TimePicker: added history. * TimePicker: cleaned up snapshot data. * TimePicker: fixed so we keep the quick values in the input. * TimePicker: fixed the missing styling on UTC. * TimePicker: added missing caret icon. * TimePicker: fixed formatting on recent time ranges. * TimePicker: added missing -. * TimePicker: refactorings after feedback. * TimePicker: renamed reserved prop name. * TimePicker: added missing onChange call. * TimePicker: removed alternative return type. * TimePicker: fixed the sorting order on the recent list. * TimePicker: added useCallback for the onEvent functions. * TimePicker: moving away from default export. * TimePicker: used the isMathString instead of private function. * TimePicker: minor refactoring simplify the code. * TimePicker: Added empty container that will expand when less then 4 recent searches. * TimePicker: changed the top to be absolute relative to the container. * TimePicker: updated snapshots for failing tests. * Fixed shadow * Move it down a bit * added some more tests. * Fixed so we change the anchor point of the time picker in really small screens. * removed memo. * fixed snapshot. * Make sure that we always use the correct timeZone when formatting output. * Fixed form background. * Some minor fixes after demo. * Making sure that empty info box is centered. * updated snapshots for timepicker after css changes. * fixed so we don't overflow when input validation error. * adjusted final things on the time picker. Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
104c2e3636
commit
587e4009f3
@ -78,14 +78,14 @@ export const Field: React.FC<FieldProps> = ({
|
||||
)}
|
||||
<div>
|
||||
{React.cloneElement(children, { invalid, disabled, loading })}
|
||||
{error && !horizontal && (
|
||||
{invalid && error && !horizontal && (
|
||||
<div className={styles.fieldValidationWrapper}>
|
||||
<FieldValidationMessage>{error}</FieldValidationMessage>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && horizontal && (
|
||||
{invalid && error && horizontal && (
|
||||
<div className={cx(styles.fieldValidationWrapper, styles.fieldValidationWrapperHorizontal)}>
|
||||
<FieldValidationMessage>{error}</FieldValidationMessage>
|
||||
</div>
|
||||
|
@ -115,7 +115,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
||||
|
||||
input: cx(
|
||||
getFocusStyle(theme),
|
||||
sharedInputStyle(theme),
|
||||
sharedInputStyle(theme, invalid),
|
||||
css`
|
||||
label: input-input;
|
||||
position: relative;
|
||||
@ -211,8 +211,8 @@ export const Input: FC<Props> = props => {
|
||||
*/
|
||||
const [prefixRect, prefixRef] = useClientRect<HTMLDivElement>();
|
||||
const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>();
|
||||
const theme = useTheme();
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getInputStyles({ theme, invalid: !!invalid });
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { getFormStyles } from './getFormStyles';
|
||||
import { Label } from './Label';
|
||||
import { Input } from './Input/Input';
|
||||
import { Form } from './Form';
|
||||
import { Field } from './Field';
|
||||
import { Button } from './Button';
|
||||
|
||||
const Forms = {
|
||||
getFormStyles,
|
||||
Label: Label,
|
||||
Input: Input,
|
||||
Button: Button,
|
||||
Label,
|
||||
Input,
|
||||
Form,
|
||||
Field,
|
||||
Button,
|
||||
};
|
||||
|
||||
export default Forms;
|
||||
|
@ -4,12 +4,12 @@ import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { TimePicker } from './TimePicker';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withRightAlignedStory } from '../../utils/storybook/withRightAlignedStory';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TimeFragment, dateTime } from '@grafana/data';
|
||||
|
||||
const TimePickerStories = storiesOf('UI/TimePicker', module);
|
||||
|
||||
TimePickerStories.addDecorator(withRightAlignedStory);
|
||||
TimePickerStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimePickerStories.add('default', () => {
|
||||
return (
|
||||
@ -25,16 +25,6 @@ TimePickerStories.add('default', () => {
|
||||
<TimePicker
|
||||
timeZone="browser"
|
||||
value={value}
|
||||
selectOptions={[
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
|
||||
]}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
|
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UnthemedTimePicker } from './TimePicker';
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
import dark from './../../themes/dark';
|
||||
|
||||
const from = '2019-12-17T07:48:27.433Z';
|
||||
const to = '2019-12-18T07:48:27.433Z';
|
||||
|
||||
const value: TimeRange = {
|
||||
from: dateTime(from),
|
||||
to: dateTime(to),
|
||||
raw: { from: dateTime(from), to: dateTime(to) },
|
||||
};
|
||||
|
||||
describe('TimePicker', () => {
|
||||
it('renders buttons correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<UnthemedTimePicker
|
||||
onChange={value => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
theme={dark}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders content correctly after beeing open', () => {
|
||||
const wrapper = shallow(
|
||||
<UnthemedTimePicker
|
||||
onChange={value => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
theme={dark}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('button[aria-label="TimePicker Open Button"]').simulate('click', new Event('click'));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,60 +1,23 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
import React, { PureComponent, memo, FormEvent } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Components
|
||||
import { ButtonSelect } from '../Select/ButtonSelect';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { TimePickerPopover } from './TimePickerPopover';
|
||||
import { TimePickerContent } from './TimePickerContent/TimePickerContent';
|
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
|
||||
// Utils & Services
|
||||
import { isDateTime, DateTime, rangeUtil } from '@grafana/data';
|
||||
import { rawToTimeRange } from './time';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
import { withTheme, useTheme } from '../../themes/ThemeContext';
|
||||
|
||||
// Types
|
||||
import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue, dateMath, GrafanaTheme } from '@grafana/data';
|
||||
import { isDateTime, DateTime, rangeUtil, GrafanaTheme, TIME_FORMAT } from '@grafana/data';
|
||||
import { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data';
|
||||
import { Themeable } from '../../types';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
timePickerSynced: css`
|
||||
label: timePickerSynced;
|
||||
border-color: ${theme.colors.orangeDark};
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
color: ${theme.colors.orangeDark};
|
||||
&:focus,
|
||||
:hover {
|
||||
color: ${theme.colors.orangeDark};
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
noRightBorderStyle: css`
|
||||
label: noRightBorderStyle;
|
||||
border-right: 0;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export interface Props extends Themeable {
|
||||
hideText?: boolean;
|
||||
value: TimeRange;
|
||||
selectOptions: TimeOption[];
|
||||
timeZone?: TimeZone;
|
||||
timeSyncButton?: JSX.Element;
|
||||
isSynced?: boolean;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
onMoveBackward: () => void;
|
||||
onMoveForward: () => void;
|
||||
onZoom: () => void;
|
||||
}
|
||||
|
||||
export const defaultSelectOptions: TimeOption[] = [
|
||||
const quickOptions: TimeOption[] = [
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
@ -71,6 +34,9 @@ export const defaultSelectOptions: TimeOption[] = [
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 },
|
||||
];
|
||||
|
||||
const otherOptions: TimeOption[] = [
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 },
|
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 },
|
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 },
|
||||
@ -87,69 +53,79 @@ export const defaultSelectOptions: TimeOption[] = [
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 3 },
|
||||
];
|
||||
|
||||
const defaultZoomOutTooltip = () => {
|
||||
return (
|
||||
<>
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</>
|
||||
);
|
||||
};
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
`,
|
||||
buttons: css`
|
||||
display: flex;
|
||||
`,
|
||||
caretIcon: css`
|
||||
margin-left: 3px;
|
||||
|
||||
i {
|
||||
font-size: ${theme.typography.size.md};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
display: inline-block;
|
||||
`,
|
||||
utc: css`
|
||||
color: ${theme.colors.orange};
|
||||
font-size: 75%;
|
||||
padding: 3px;
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export interface Props extends Themeable {
|
||||
hideText?: boolean;
|
||||
value: TimeRange;
|
||||
timeZone?: TimeZone;
|
||||
timeSyncButton?: JSX.Element;
|
||||
isSynced?: boolean;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
onMoveBackward: () => void;
|
||||
onMoveForward: () => void;
|
||||
onZoom: () => void;
|
||||
history?: TimeRange[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isCustomOpen: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
class UnThemedTimePicker extends PureComponent<Props, State> {
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
|
||||
export class UnthemedTimePicker extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isCustomOpen: false,
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
mapTimeOptionsToSelectableValues = (selectOptions: TimeOption[]) => {
|
||||
const options = selectOptions.map(timeOption => {
|
||||
return {
|
||||
label: timeOption.display,
|
||||
value: timeOption,
|
||||
};
|
||||
});
|
||||
|
||||
options.unshift({
|
||||
label: 'Custom time range',
|
||||
value: { from: 'custom', to: 'custom', display: 'Custom', section: 1 },
|
||||
});
|
||||
|
||||
return options;
|
||||
onChange = (timeRange: TimeRange) => {
|
||||
this.props.onChange(timeRange);
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
onSelectChanged = (item: SelectableValue<TimeOption>) => {
|
||||
const { onChange, timeZone } = this.props;
|
||||
|
||||
if (item.value && item.value.from === 'custom') {
|
||||
// this is to prevent the ClickOutsideWrapper from directly closing the popover
|
||||
setTimeout(() => {
|
||||
this.setState({ isCustomOpen: true });
|
||||
}, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.value) {
|
||||
onChange(rawToTimeRange({ from: item.value.from, to: item.value.to }, timeZone));
|
||||
}
|
||||
onOpen = (event: FormEvent<HTMLButtonElement>) => {
|
||||
const { isOpen } = this.state;
|
||||
event.stopPropagation();
|
||||
this.setState({ isOpen: !isOpen });
|
||||
};
|
||||
|
||||
onCustomChange = (timeRange: TimeRange) => {
|
||||
const { onChange } = this.props;
|
||||
onChange(timeRange);
|
||||
this.setState({ isCustomOpen: false });
|
||||
};
|
||||
|
||||
onCloseCustom = () => {
|
||||
this.setState({ isCustomOpen: false });
|
||||
onClose = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectOptions: selectTimeOptions,
|
||||
value,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
@ -158,56 +134,48 @@ class UnThemedTimePicker extends PureComponent<Props, State> {
|
||||
timeSyncButton,
|
||||
isSynced,
|
||||
theme,
|
||||
hideText,
|
||||
history,
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
const styles = getStyles(theme);
|
||||
const { isCustomOpen } = this.state;
|
||||
const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions);
|
||||
const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value));
|
||||
|
||||
const isUTC = timeZone === 'utc';
|
||||
|
||||
const adjustedTime = (time: DateTime) => (isUTC ? time.utc() : time.local()) || null;
|
||||
const adjustedTimeRange = {
|
||||
to: dateMath.isMathString(value.raw.to) ? value.raw.to : adjustedTime(value.to),
|
||||
from: dateMath.isMathString(value.raw.from) ? value.raw.from : adjustedTime(value.from),
|
||||
};
|
||||
const rangeString = rangeUtil.describeTimeRange(adjustedTimeRange);
|
||||
|
||||
const label = !hideText ? (
|
||||
<>
|
||||
{isCustomOpen && <span>Custom time range</span>}
|
||||
{!isCustomOpen && <span>{rangeString}</span>}
|
||||
{isUTC && <span className="time-picker-utc">UTC</span>}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||
|
||||
return (
|
||||
<div className="time-picker" ref={this.pickerTriggerRef}>
|
||||
<div className="time-picker-buttons">
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttons}>
|
||||
{hasAbsolute && (
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
|
||||
<i className="fa fa-chevron-left" />
|
||||
</button>
|
||||
)}
|
||||
<ButtonSelect
|
||||
className={classNames('time-picker-button-select', {
|
||||
['explore-active-button-glow']: timeSyncButton && isSynced,
|
||||
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: timeSyncButton,
|
||||
[styles.timePickerSynced]: timeSyncButton ? isSynced : null,
|
||||
})}
|
||||
value={currentOption}
|
||||
label={label}
|
||||
options={options}
|
||||
maxMenuHeight={600}
|
||||
onChange={this.onSelectChanged}
|
||||
iconClass={classNames('fa fa-clock-o fa-fw', isSynced && timeSyncButton && 'icon-brand-gradient')}
|
||||
tooltipContent={<TimePickerTooltipContent timeRange={value} />}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip content={<TimePickerTooltip timeRange={value} />} placement="bottom">
|
||||
<button
|
||||
aria-label="TimePicker Open Button"
|
||||
className="btn navbar-button navbar-button--zoom"
|
||||
onClick={this.onOpen}
|
||||
>
|
||||
<i className={classNames('fa fa-clock-o fa-fw', isSynced && timeSyncButton && 'icon-brand-gradient')} />
|
||||
<TimePickerButtonLabel {...this.props} />
|
||||
<span className={styles.caretIcon}>
|
||||
{isOpen ? <i className="fa fa-caret-up fa-fw" /> : <i className="fa fa-caret-down fa-fw" />}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<ClickOutsideWrapper onClick={this.onClose}>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
otherOptions={otherOptions}
|
||||
quickOptions={quickOptions}
|
||||
history={history}
|
||||
/>
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{timeSyncButton}
|
||||
|
||||
@ -217,24 +185,24 @@ class UnThemedTimePicker extends PureComponent<Props, State> {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Tooltip content={defaultZoomOutTooltip} placement="bottom">
|
||||
<Tooltip content={ZoomOutTooltip} placement="bottom">
|
||||
<button className="btn navbar-button navbar-button--zoom" onClick={onZoom}>
|
||||
<i className="fa fa-search-minus" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{isCustomOpen && (
|
||||
<ClickOutsideWrapper onClick={this.onCloseCustom}>
|
||||
<TimePickerPopover value={value} timeZone={timeZone} onChange={this.onCustomChange} />
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => (
|
||||
const ZoomOutTooltip = () => (
|
||||
<>
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</>
|
||||
);
|
||||
|
||||
const TimePickerTooltip = ({ timeRange }: { timeRange: TimeRange }) => (
|
||||
<>
|
||||
{timeRange.from.format(TIME_FORMAT)}
|
||||
<div className="text-center">to</div>
|
||||
@ -242,8 +210,36 @@ const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean {
|
||||
return range.raw.from === option.from && range.raw.to === option.to;
|
||||
}
|
||||
const TimePickerButtonLabel = memo<Props>(props => {
|
||||
const theme = useTheme();
|
||||
const styles = getLabelStyles(theme);
|
||||
const isUTC = props.timeZone === 'utc';
|
||||
|
||||
export const TimePicker = withTheme(UnThemedTimePicker);
|
||||
if (props.hideText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.container}>
|
||||
<span>{formattedRange(props.value, isUTC)}</span>
|
||||
{isUTC && <span className={styles.utc}>UTC</span>}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
const formattedRange = (value: TimeRange, isUTC: boolean) => {
|
||||
const adjustedTimeRange = {
|
||||
to: dateMath.isMathString(value.raw.to) ? value.raw.to : adjustedTime(value.to, isUTC),
|
||||
from: dateMath.isMathString(value.raw.from) ? value.raw.from : adjustedTime(value.from, isUTC),
|
||||
};
|
||||
return rangeUtil.describeTimeRange(adjustedTimeRange);
|
||||
};
|
||||
|
||||
const adjustedTime = (time: DateTime, isUTC: boolean) => {
|
||||
if (isUTC) {
|
||||
return time.utc() || null;
|
||||
}
|
||||
return time.local() || null;
|
||||
};
|
||||
|
||||
export const TimePicker = withTheme(UnthemedTimePicker);
|
||||
|
@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { TimeFragment } from '@grafana/data';
|
||||
|
||||
const TimePickerCalendarStories = storiesOf('UI/TimePicker/TimePickerCalendar', module);
|
||||
|
||||
TimePickerCalendarStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimePickerCalendarStories.add('default', () => (
|
||||
<UseState initialState={'now-6h' as TimeFragment}>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePickerCalendar
|
||||
timeZone="browser"
|
||||
value={value}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
));
|
@ -1,61 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||
import { TimeFragment, TimeZone, TIME_FORMAT } from '@grafana/data';
|
||||
import { DateTime, dateTime, toUtc } from '@grafana/data';
|
||||
import { stringToDateTimeType } from './time';
|
||||
|
||||
export interface Props {
|
||||
value: TimeFragment;
|
||||
roundup?: boolean;
|
||||
timeZone?: TimeZone;
|
||||
onChange: (value: DateTime) => void;
|
||||
}
|
||||
|
||||
export class TimePickerCalendar extends PureComponent<Props> {
|
||||
onCalendarChange = (date: Date | Date[]) => {
|
||||
const { onChange, timeZone } = this.props;
|
||||
|
||||
if (Array.isArray(date)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newDate = dateTime(date);
|
||||
|
||||
if (timeZone === 'utc') {
|
||||
newDate = toUtc(newDate.format(TIME_FORMAT));
|
||||
}
|
||||
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
onDrilldown = (props: any) => {
|
||||
// this is to prevent clickout side wrapper from triggering when drilling down
|
||||
if (window.event) {
|
||||
// @ts-ignore
|
||||
window.event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, roundup, timeZone } = this.props;
|
||||
let date = stringToDateTimeType(value, roundup, timeZone);
|
||||
|
||||
if (!date.isValid()) {
|
||||
date = dateTime();
|
||||
}
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
value={date.toDate()}
|
||||
next2Label={null}
|
||||
prev2Label={null}
|
||||
className="time-picker-calendar"
|
||||
tileClassName="time-picker-calendar-tile"
|
||||
onChange={this.onCalendarChange}
|
||||
onDrillDown={this.onDrilldown}
|
||||
nextLabel={<span className="fa fa-angle-right" />}
|
||||
prevLabel={<span className="fa fa-angle-left" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,305 @@
|
||||
import React, { memo } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||
import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data';
|
||||
import { stringToDateTimeType } from '../time';
|
||||
import { useTheme, stylesFactory } from '../../../themes';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import Forms from '../../Forms';
|
||||
import { Portal } from '../../Portal/Portal';
|
||||
import { getThemeColors } from './colors';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
top: 0;
|
||||
position: absolute;
|
||||
right: 546px;
|
||||
box-shadow: 0px 0px 20px ${colors.shadow};
|
||||
background-color: ${colors.background};
|
||||
z-index: -1;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
background-color: ${colors.background};
|
||||
width: 19px;
|
||||
height: 381px;
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -19px;
|
||||
border-left: 1px solid ${colors.border};
|
||||
}
|
||||
`,
|
||||
modal: css`
|
||||
position: fixed;
|
||||
top: 20%;
|
||||
width: 100%;
|
||||
z-index: ${theme.zIndex.modal};
|
||||
`,
|
||||
content: css`
|
||||
margin: 0 auto;
|
||||
width: 268px;
|
||||
`,
|
||||
backdrop: css`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #202226;
|
||||
opacity: 0.7;
|
||||
z-index: ${theme.zIndex.modalBackdrop};
|
||||
text-align: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getFooterStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${colors.background};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
align-items: stretch;
|
||||
`,
|
||||
apply: css`
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
title: css`
|
||||
color: ${theme.colors.text}
|
||||
background-color: ${colors.background};
|
||||
line-height: 21px;
|
||||
font-size: ${theme.typography.size.md};
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
body: css`
|
||||
z-index: ${theme.zIndex.modal};
|
||||
background-color: ${colors.background};
|
||||
width: 268px;
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
padding-top: 4px;
|
||||
background-color: inherit;
|
||||
color: ${theme.colors.text};
|
||||
border: 0;
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
background-color: inherit;
|
||||
text-align: center;
|
||||
color: ${theme.colors.blueShade};
|
||||
|
||||
abbr {
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 4px 0 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.react-calendar__tile,
|
||||
.react-calendar__tile--now {
|
||||
margin-bottom: 4px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation > button:focus,
|
||||
.time-picker-calendar-tile:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active,
|
||||
.react-calendar__tile--active:hover {
|
||||
color: ${theme.colors.white};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
background: ${theme.colors.blue95};
|
||||
box-shadow: none;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd,
|
||||
.react-calendar__tile--rangeStart {
|
||||
padding: 0;
|
||||
border: 0px;
|
||||
color: ${theme.colors.white};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
background: ${theme.colors.blue95};
|
||||
|
||||
abbr {
|
||||
background-color: ${theme.colors.blue77};
|
||||
border-radius: 100px;
|
||||
display: block;
|
||||
padding: 2px 7px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart {
|
||||
border-top-left-radius: 20px;
|
||||
border-bottom-left-radius: 20px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd {
|
||||
border-top-right-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getHeaderStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${colors.background};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 7px;
|
||||
`,
|
||||
close: css`
|
||||
cursor: pointer;
|
||||
font-size: ${theme.typography.size.lg};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
from: string;
|
||||
to: string;
|
||||
onClose: () => void;
|
||||
onApply: () => void;
|
||||
onChange: (from: string, to: string) => void;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export const TimePickerCalendar = memo<Props>(props => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const { isOpen, isFullscreen } = props;
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<ClickOutsideWrapper onClick={props.onClose}>
|
||||
<div className={styles.container}>
|
||||
<Body {...props} />
|
||||
</div>
|
||||
</ClickOutsideWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className={styles.modal} onClick={event => event.stopPropagation()}>
|
||||
<div className={styles.content}>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
<Footer {...props} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.backdrop} onClick={event => event.stopPropagation()} />
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
const Header = memo<Props>(({ onClose }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getHeaderStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TimePickerTitle>Select a time range</TimePickerTitle>
|
||||
<i className={cx(styles.close, 'fa', 'fa-times')} onClick={onClose} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Body = memo<Props>(props => {
|
||||
const theme = useTheme();
|
||||
const styles = getBodyStyles(theme);
|
||||
const { from, to, onChange } = props;
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
selectRange={true}
|
||||
next2Label={null}
|
||||
prev2Label={null}
|
||||
className={styles.body}
|
||||
tileClassName={styles.title}
|
||||
value={inputToValue(from, to)}
|
||||
nextLabel={<span className="fa fa-angle-right" />}
|
||||
prevLabel={<span className="fa fa-angle-left" />}
|
||||
onChange={value => valueToInput(value, onChange)}
|
||||
locale="en"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const Footer = memo<Props>(({ onClose, onApply }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getFooterStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Forms.Button className={styles.apply} onClick={onApply}>
|
||||
Apply time range
|
||||
</Forms.Button>
|
||||
<Forms.Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Forms.Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function inputToValue(from: string, to: string): Date[] {
|
||||
const fromAsDateTime = stringToDateTimeType(from);
|
||||
const toAsDateTime = stringToDateTimeType(to);
|
||||
const fromAsDate = fromAsDateTime.isValid() ? fromAsDateTime.toDate() : new Date();
|
||||
const toAsDate = toAsDateTime.isValid() ? toAsDateTime.toDate() : new Date();
|
||||
|
||||
if (fromAsDate > toAsDate) {
|
||||
return [toAsDate, fromAsDate];
|
||||
}
|
||||
return [fromAsDate, toAsDate];
|
||||
}
|
||||
|
||||
function valueToInput(value: Date | Date[], onChange: (from: string, to: string) => void): void {
|
||||
const [from, to] = value;
|
||||
const fromAsString = dateTime(from).format(TIME_FORMAT);
|
||||
const toAsString = dateTime(to).format(TIME_FORMAT);
|
||||
|
||||
return onChange(fromAsString, toAsString);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TimePickerContentWithScreenSize } from './TimePickerContent';
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
|
||||
describe('TimePickerContent', () => {
|
||||
it('renders correctly in full screen', () => {
|
||||
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z');
|
||||
const wrapper = shallow(
|
||||
<TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={true} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly in narrow screen', () => {
|
||||
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z');
|
||||
const wrapper = shallow(
|
||||
<TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={false} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders recent absolute ranges correctly', () => {
|
||||
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z');
|
||||
const history = [
|
||||
createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'),
|
||||
createTimeRange('2019-10-17T07:48:27.433Z', '2019-10-18T07:48:27.433Z'),
|
||||
];
|
||||
|
||||
const wrapper = shallow(
|
||||
<TimePickerContentWithScreenSize
|
||||
onChange={value => {}}
|
||||
timeZone="utc"
|
||||
value={value}
|
||||
isFullscreen={true}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
function createTimeRange(from: string, to: string): TimeRange {
|
||||
return {
|
||||
from: dateTime(from),
|
||||
to: dateTime(to),
|
||||
raw: { from: dateTime(from), to: dateTime(to) },
|
||||
};
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
import React, { useState, memo } from 'react';
|
||||
import { useMedia } from 'react-use';
|
||||
import { css } from 'emotion';
|
||||
import { useTheme, stylesFactory } from '../../../themes';
|
||||
import { GrafanaTheme, TimeOption, TimeRange, TimeZone, isDateTime } from '@grafana/data';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { TimeRangeForm } from './TimeRangeForm';
|
||||
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
|
||||
import { TimeRangeList } from './TimeRangeList';
|
||||
import { mapRangeToTimeOption } from './mapper';
|
||||
import { getThemeColors } from './colors';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
background: ${colors.background};
|
||||
box-shadow: 0px 0px 20px ${colors.shadow};
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.modal};
|
||||
width: 546px;
|
||||
height: 381px;
|
||||
top: 116%;
|
||||
margin-left: -322px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
width: 218px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
width: 264px;
|
||||
margin-left: -100px;
|
||||
}
|
||||
`,
|
||||
leftSide: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid ${colors.border};
|
||||
width: 60%;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
rightSide: css`
|
||||
width: 40% !important;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
width: 100% !important;
|
||||
}
|
||||
`,
|
||||
spacing: css`
|
||||
margin-top: 16px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
header: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
padding: 7px 9px 7px 9px;
|
||||
`,
|
||||
body: css`
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
background: ${colors.formBackground};
|
||||
box-shadow: inset 0px 2px 2px ${colors.shadow};
|
||||
`,
|
||||
form: css`
|
||||
padding: 7px 9px 7px 9px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getFullScreenStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
padding-top: 9px;
|
||||
padding-left: 11px;
|
||||
padding-right: 20%;
|
||||
`,
|
||||
title: css`
|
||||
margin-bottom: 11px;
|
||||
`,
|
||||
recent: css`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getEmptyListStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const colors = getThemeColors(theme);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${colors.formBackground};
|
||||
padding: 12px;
|
||||
margin: 12px;
|
||||
|
||||
a,
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
`,
|
||||
link: css`
|
||||
color: ${theme.colors.linkExternal};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface Props {
|
||||
value: TimeRange;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
timeZone?: TimeZone;
|
||||
quickOptions?: TimeOption[];
|
||||
otherOptions?: TimeOption[];
|
||||
history?: TimeRange[];
|
||||
}
|
||||
|
||||
interface PropsWithScreenSize extends Props {
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
interface FormProps extends Omit<Props, 'history'> {
|
||||
visible: boolean;
|
||||
historyOptions?: TimeOption[];
|
||||
}
|
||||
|
||||
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = props => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const historyOptions = mapToHistoryOptions(props.history, props.timeZone);
|
||||
const { quickOptions = [], otherOptions = [], isFullscreen } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftSide}>
|
||||
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} />
|
||||
</div>
|
||||
<CustomScrollbar className={styles.rightSide}>
|
||||
<NarrowScreenForm {...props} visible={!isFullscreen} historyOptions={historyOptions} />
|
||||
<TimeRangeList
|
||||
title="Relative time ranges"
|
||||
options={quickOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
<div className={styles.spacing} />
|
||||
<TimeRangeList
|
||||
title="Other quick ranges"
|
||||
options={otherOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimePickerContent: React.FC<Props> = props => {
|
||||
const theme = useTheme();
|
||||
const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.lg})`);
|
||||
|
||||
return <TimePickerContentWithScreenSize {...props} isFullscreen={isFullscreen} />;
|
||||
};
|
||||
|
||||
const NarrowScreenForm: React.FC<FormProps> = props => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getNarrowScreenStyles(theme);
|
||||
const isAbsolute = isDateTime(props.value.raw.from) || isDateTime(props.value.raw.to);
|
||||
const [collapsed, setCollapsed] = useState(isAbsolute);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.header} onClick={() => setCollapsed(!collapsed)}>
|
||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||
{collapsed ? <i className="fa fa-caret-up" /> : <i className="fa fa-caret-down" />}
|
||||
</div>
|
||||
{collapsed && (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.form}>
|
||||
<TimeRangeForm
|
||||
value={props.value}
|
||||
onApply={props.onChange}
|
||||
timeZone={props.timeZone}
|
||||
isFullscreen={false}
|
||||
/>
|
||||
</div>
|
||||
<TimeRangeList
|
||||
title="Recently used absolute ranges"
|
||||
options={props.historyOptions || []}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
placeholderEmpty={null}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FullScreenForm: React.FC<FormProps> = props => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getFullScreenStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||
</div>
|
||||
<TimeRangeForm value={props.value} timeZone={props.timeZone} onApply={props.onChange} isFullscreen={true} />
|
||||
</div>
|
||||
<div className={styles.recent}>
|
||||
<TimeRangeList
|
||||
title="Recently used absolute ranges"
|
||||
options={props.historyOptions || []}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
placeholderEmpty={<EmptyRecentList />}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyRecentList = memo(() => {
|
||||
const theme = useTheme();
|
||||
const styles = getEmptyListStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<span>
|
||||
It looks like you haven't used this timer picker before. As soon as you enter some time intervals, recently
|
||||
used intervals will appear here.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<a className={styles.link} href="https://grafana.com/docs/grafana/latest/reference/timerange/" target="_new">
|
||||
Read the documentation
|
||||
</a>
|
||||
<span> to find out more about how to enter custom time ranges.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOption[] {
|
||||
if (!Array.isArray(ranges) || ranges.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return ranges.slice(ranges.length - 4).map(range => mapRangeToTimeOption(range, timeZone));
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import React, { memo, PropsWithChildren } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useTheme, stylesFactory } from '../../../themes';
|
||||
|
||||
const getStyle = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
text: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
color: ${theme.colors.formLabel};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const TimePickerTitle = memo<PropsWithChildren<{}>>(({ children }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyle(theme);
|
||||
|
||||
return <span className={styles.text}>{children}</span>;
|
||||
});
|
@ -0,0 +1,125 @@
|
||||
import React, { FormEvent, useState, useCallback } from 'react';
|
||||
import { TIME_FORMAT, TimeZone, isDateTime, TimeRange, DateTime } from '@grafana/data';
|
||||
import { stringToDateTimeType, isValidTimeString } from '../time';
|
||||
import { mapStringsToTimeRange } from './mapper';
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import Forms from '../../Forms';
|
||||
import { isMathString } from '@grafana/data/src/datetime/datemath';
|
||||
|
||||
interface Props {
|
||||
isFullscreen: boolean;
|
||||
value: TimeRange;
|
||||
onApply: (range: TimeRange) => void;
|
||||
timeZone?: TimeZone;
|
||||
roundup?: boolean;
|
||||
}
|
||||
|
||||
interface InputState {
|
||||
value: string;
|
||||
invalid: boolean;
|
||||
}
|
||||
|
||||
const errorMessage = 'Please enter a past date or "now"';
|
||||
|
||||
export const TimeRangeForm: React.FC<Props> = props => {
|
||||
const { value, isFullscreen = false, timeZone, roundup } = props;
|
||||
|
||||
const [from, setFrom] = useState<InputState>(valueToState(value.raw.from, false, timeZone));
|
||||
const [to, setTo] = useState<InputState>(valueToState(value.raw.to, true, timeZone));
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const onOpen = useCallback(
|
||||
(event: FormEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
setOpen(true);
|
||||
},
|
||||
[setOpen]
|
||||
);
|
||||
|
||||
const onFocus = useCallback(
|
||||
(event: FormEvent<HTMLElement>) => {
|
||||
if (!isFullscreen) {
|
||||
return;
|
||||
}
|
||||
onOpen(event);
|
||||
},
|
||||
[isFullscreen, onOpen]
|
||||
);
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
if (to.invalid || from.invalid) {
|
||||
return;
|
||||
}
|
||||
props.onApply(mapStringsToTimeRange(from.value, to.value, roundup, timeZone));
|
||||
}, [from, to, roundup, timeZone]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(from: string, to: string) => {
|
||||
setFrom(valueToState(from, false, timeZone));
|
||||
setTo(valueToState(to, true, timeZone));
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
const icon = isFullscreen ? null : <Forms.Button icon="fa fa-calendar" variant="secondary" onClick={onOpen} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.Field label="From" invalid={from.invalid} error={errorMessage}>
|
||||
<Forms.Input
|
||||
onClick={event => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={event => setFrom(eventToState(event, false, timeZone))}
|
||||
addonAfter={icon}
|
||||
value={from.value}
|
||||
/>
|
||||
</Forms.Field>
|
||||
<Forms.Field label="To" invalid={to.invalid} error={errorMessage}>
|
||||
<Forms.Input
|
||||
onClick={event => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={event => setTo(eventToState(event, true, timeZone))}
|
||||
addonAfter={icon}
|
||||
value={to.value}
|
||||
/>
|
||||
</Forms.Field>
|
||||
<Forms.Button onClick={onApply}>Apply time range</Forms.Button>
|
||||
|
||||
<TimePickerCalendar
|
||||
isFullscreen={isFullscreen}
|
||||
isOpen={isOpen}
|
||||
from={from.value}
|
||||
to={to.value}
|
||||
onApply={onApply}
|
||||
onClose={() => setOpen(false)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function eventToState(event: FormEvent<HTMLInputElement>, roundup?: boolean, timeZone?: TimeZone): InputState {
|
||||
return valueToState(event.currentTarget.value, roundup, timeZone);
|
||||
}
|
||||
|
||||
function valueToState(raw: DateTime | string, roundup?: boolean, timeZone?: TimeZone): InputState {
|
||||
const value = valueAsString(raw);
|
||||
const invalid = !isValid(value, roundup, timeZone);
|
||||
return { value, invalid };
|
||||
}
|
||||
|
||||
function valueAsString(value: DateTime | string): string {
|
||||
if (isDateTime(value)) {
|
||||
return value.format(TIME_FORMAT);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isValid(value: string, roundup?: boolean, timeZone?: TimeZone): boolean {
|
||||
if (isMathString(value)) {
|
||||
return isValidTimeString(value);
|
||||
}
|
||||
|
||||
const parsed = stringToDateTimeType(value, roundup, timeZone);
|
||||
return parsed.isValid();
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { TimeOption, TimeZone } from '@grafana/data';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { TimeRangeOption } from './TimeRangeOption';
|
||||
import { mapOptionToTimeRange } from './mapper';
|
||||
import { stylesFactory } from '../../../themes';
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
title: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px 5px 9px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getOptionsStyles = stylesFactory(() => {
|
||||
return {
|
||||
grow: css`
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
options: TimeOption[];
|
||||
value?: TimeRange;
|
||||
onSelect: (option: TimeRange) => void;
|
||||
placeholderEmpty?: ReactNode;
|
||||
timeZone?: TimeZone;
|
||||
}
|
||||
|
||||
export const TimeRangeList: React.FC<Props> = props => {
|
||||
const styles = getStyles();
|
||||
const { title, options, placeholderEmpty } = props;
|
||||
|
||||
if (typeof placeholderEmpty !== 'undefined' && options.length <= 0) {
|
||||
return <>{placeholderEmpty}</>;
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return <Options {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.title}>
|
||||
<TimePickerTitle>{title}</TimePickerTitle>
|
||||
</div>
|
||||
<Options {...props} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => {
|
||||
const styles = getOptionsStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{options.map((option, index) => (
|
||||
<TimeRangeOption
|
||||
key={keyForOption(option, index)}
|
||||
value={option}
|
||||
selected={isEqual(option, value)}
|
||||
onSelect={option => onSelect(mapOptionToTimeRange(option, timeZone))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.grow}></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function keyForOption(option: TimeOption, index: number): string {
|
||||
return `${option.from}-${option.to}-${index}`;
|
||||
}
|
||||
|
||||
function isEqual(x: TimeOption, y?: TimeRange): boolean {
|
||||
if (!y || !x) {
|
||||
return false;
|
||||
}
|
||||
return y.raw.from === x.from && y.raw.to === x.to;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import React, { memo } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme, TimeOption } from '@grafana/data';
|
||||
import { useTheme, stylesFactory, selectThemeVariant } from '../../../themes';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const background = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray7,
|
||||
dark: theme.colors.dark3,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px 9px 7px 9px;
|
||||
border-left: 2px solid rgba(255, 255, 255, 0);
|
||||
|
||||
&:hover {
|
||||
background: ${background};
|
||||
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
|
||||
border-image-slice: 1;
|
||||
border-style: solid;
|
||||
border-top: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-left-width: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface Props {
|
||||
value: TimeOption;
|
||||
selected?: boolean;
|
||||
onSelect: (option: TimeOption) => void;
|
||||
}
|
||||
|
||||
export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container} onClick={() => onSelect(value)} tabIndex={-1}>
|
||||
<span>{value.display}</span>
|
||||
{selected ? <i className="fa fa-check" /> : null}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,344 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TimePickerContent renders correctly in full screen 1`] = `
|
||||
<div
|
||||
className="css-1fbt695"
|
||||
>
|
||||
<div
|
||||
className="css-13dsoi7"
|
||||
>
|
||||
<FullScreenForm
|
||||
historyOptions={Array []}
|
||||
isFullscreen={true}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={true}
|
||||
/>
|
||||
</div>
|
||||
<CustomScrollbar
|
||||
autoHeightMax="100%"
|
||||
autoHeightMin="0"
|
||||
autoHide={false}
|
||||
autoHideDuration={200}
|
||||
autoHideTimeout={200}
|
||||
className="css-1o1b8dr"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
setScrollTop={[Function]}
|
||||
>
|
||||
<NarrowScreenForm
|
||||
historyOptions={Array []}
|
||||
isFullscreen={true}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={false}
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Relative time ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="css-1ogeuxc"
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Other quick ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TimePickerContent renders correctly in narrow screen 1`] = `
|
||||
<div
|
||||
className="css-1fbt695"
|
||||
>
|
||||
<div
|
||||
className="css-13dsoi7"
|
||||
>
|
||||
<FullScreenForm
|
||||
historyOptions={Array []}
|
||||
isFullscreen={false}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={false}
|
||||
/>
|
||||
</div>
|
||||
<CustomScrollbar
|
||||
autoHeightMax="100%"
|
||||
autoHeightMin="0"
|
||||
autoHide={false}
|
||||
autoHideDuration={200}
|
||||
autoHideTimeout={200}
|
||||
className="css-1o1b8dr"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
setScrollTop={[Function]}
|
||||
>
|
||||
<NarrowScreenForm
|
||||
historyOptions={Array []}
|
||||
isFullscreen={false}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={true}
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Relative time ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="css-1ogeuxc"
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Other quick ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
|
||||
<div
|
||||
className="css-1fbt695"
|
||||
>
|
||||
<div
|
||||
className="css-13dsoi7"
|
||||
>
|
||||
<FullScreenForm
|
||||
history={
|
||||
Array [
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
Object {
|
||||
"from": "2019-10-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-10-17T07:48:27.433Z",
|
||||
"to": "2019-10-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-10-18T07:48:27.433Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
historyOptions={
|
||||
Array [
|
||||
Object {
|
||||
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
|
||||
"from": "2019-12-17 07:48:27",
|
||||
"section": 3,
|
||||
"to": "2019-12-18 07:48:27",
|
||||
},
|
||||
Object {
|
||||
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
|
||||
"from": "2019-10-17 07:48:27",
|
||||
"section": 3,
|
||||
"to": "2019-10-18 07:48:27",
|
||||
},
|
||||
]
|
||||
}
|
||||
isFullscreen={true}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={true}
|
||||
/>
|
||||
</div>
|
||||
<CustomScrollbar
|
||||
autoHeightMax="100%"
|
||||
autoHeightMin="0"
|
||||
autoHide={false}
|
||||
autoHideDuration={200}
|
||||
autoHideTimeout={200}
|
||||
className="css-1o1b8dr"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
setScrollTop={[Function]}
|
||||
>
|
||||
<NarrowScreenForm
|
||||
history={
|
||||
Array [
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
Object {
|
||||
"from": "2019-10-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-10-17T07:48:27.433Z",
|
||||
"to": "2019-10-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-10-18T07:48:27.433Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
historyOptions={
|
||||
Array [
|
||||
Object {
|
||||
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
|
||||
"from": "2019-12-17 07:48:27",
|
||||
"section": 3,
|
||||
"to": "2019-12-18 07:48:27",
|
||||
},
|
||||
Object {
|
||||
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
|
||||
"from": "2019-10-17 07:48:27",
|
||||
"section": 3,
|
||||
"to": "2019-10-18 07:48:27",
|
||||
},
|
||||
]
|
||||
}
|
||||
isFullscreen={true}
|
||||
onChange={[Function]}
|
||||
timeZone="utc"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
visible={false}
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Relative time ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="css-1ogeuxc"
|
||||
/>
|
||||
<Component
|
||||
onSelect={[Function]}
|
||||
options={Array []}
|
||||
timeZone="utc"
|
||||
title="Other quick ranges"
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,35 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { selectThemeVariant } from '../../../themes/selectThemeVariant';
|
||||
|
||||
export const getThemeColors = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
border: selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray4,
|
||||
dark: theme.colors.gray25,
|
||||
},
|
||||
theme.type
|
||||
),
|
||||
background: selectThemeVariant(
|
||||
{
|
||||
dark: theme.colors.dark2,
|
||||
light: theme.background.dropdown,
|
||||
},
|
||||
theme.type
|
||||
),
|
||||
shadow: selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray85,
|
||||
dark: theme.colors.black,
|
||||
},
|
||||
theme.type
|
||||
),
|
||||
formBackground: selectThemeVariant(
|
||||
{
|
||||
dark: theme.colors.gray15,
|
||||
light: theme.colors.gray98,
|
||||
},
|
||||
theme.type
|
||||
),
|
||||
};
|
||||
};
|
@ -0,0 +1,95 @@
|
||||
import {
|
||||
TimeOption,
|
||||
TimeRange,
|
||||
isDateTime,
|
||||
DateTime,
|
||||
TimeZone,
|
||||
dateMath,
|
||||
dateTime,
|
||||
dateTimeForTimeZone,
|
||||
TIME_FORMAT,
|
||||
} from '@grafana/data';
|
||||
import { stringToDateTimeType } from '../time';
|
||||
import { isMathString } from '@grafana/data/src/datetime/datemath';
|
||||
|
||||
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
|
||||
return {
|
||||
from: stringToDateTime(option.from, false, timeZone),
|
||||
to: stringToDateTime(option.to, true, timeZone),
|
||||
raw: {
|
||||
from: option.from,
|
||||
to: option.to,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => {
|
||||
const formattedFrom = stringToDateTime(range.from, false, timeZone).format(TIME_FORMAT);
|
||||
const formattedTo = stringToDateTime(range.to, true, timeZone).format(TIME_FORMAT);
|
||||
const from = dateTimeToString(range.from, timeZone === 'utc');
|
||||
const to = dateTimeToString(range.to, timeZone === 'utc');
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
section: 3,
|
||||
display: `${formattedFrom} to ${formattedTo}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const mapStringsToTimeRange = (from: string, to: string, roundup?: boolean, timeZone?: TimeZone): TimeRange => {
|
||||
const fromDate = stringToDateTimeType(from, roundup, timeZone);
|
||||
const toDate = stringToDateTimeType(to, roundup, timeZone);
|
||||
|
||||
if (isMathString(from) || isMathString(to)) {
|
||||
return {
|
||||
from: fromDate,
|
||||
to: toDate,
|
||||
raw: {
|
||||
from,
|
||||
to,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
from: fromDate,
|
||||
to: toDate,
|
||||
raw: {
|
||||
from: fromDate,
|
||||
to: toDate,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const stringToDateTime = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => {
|
||||
if (isDateTime(value)) {
|
||||
if (timeZone === 'utc') {
|
||||
return value.utc();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.indexOf('now') !== -1) {
|
||||
if (!dateMath.isValid(value)) {
|
||||
return dateTime();
|
||||
}
|
||||
|
||||
const parsed = dateMath.parse(value, roundUp, timeZone);
|
||||
return parsed || dateTime();
|
||||
}
|
||||
|
||||
return dateTimeForTimeZone(timeZone, value, TIME_FORMAT);
|
||||
};
|
||||
|
||||
const dateTimeToString = (value: DateTime, isUtc: boolean): string => {
|
||||
if (!isDateTime(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isUtc) {
|
||||
return value.utc().format(TIME_FORMAT);
|
||||
}
|
||||
|
||||
return value.format(TIME_FORMAT);
|
||||
};
|
@ -1,60 +0,0 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { TimeFragment, TIME_FORMAT, TimeZone, isDateTime } from '@grafana/data';
|
||||
import { Input } from '../Input/Input';
|
||||
import { stringToDateTimeType, isValidTimeString } from './time';
|
||||
|
||||
export interface Props {
|
||||
value: TimeFragment;
|
||||
roundup?: boolean;
|
||||
timeZone?: TimeZone;
|
||||
onChange: (value: string, isValid: boolean) => void;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export class TimePickerInput extends PureComponent<Props> {
|
||||
isValid = (value: string) => {
|
||||
const { timeZone, roundup } = this.props;
|
||||
|
||||
if (value.indexOf('now') !== -1) {
|
||||
const isValid = isValidTimeString(value);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
const parsed = stringToDateTimeType(value, roundup, timeZone);
|
||||
const isValid = parsed.isValid();
|
||||
return isValid;
|
||||
};
|
||||
|
||||
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { onChange } = this.props;
|
||||
const value = event.target.value;
|
||||
|
||||
onChange(value, this.isValid(value));
|
||||
};
|
||||
|
||||
valueToString = (value: TimeFragment) => {
|
||||
if (isDateTime(value)) {
|
||||
return value.format(TIME_FORMAT);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, tabIndex } = this.props;
|
||||
const valueString = this.valueToString(value);
|
||||
const error = !this.isValid(valueString);
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onChange}
|
||||
hideErrorMessage={true}
|
||||
value={valueString}
|
||||
className={`time-picker-input${error ? '-error' : ''}`}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TimePickerPopover } from './TimePickerPopover';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { dateTime, DateTime } from '@grafana/data';
|
||||
|
||||
const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module);
|
||||
|
||||
TimePickerPopoverStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimePickerPopoverStories.add('default', () => (
|
||||
<UseState
|
||||
initialState={{
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
raw: { from: 'now-6h' as string | DateTime, to: 'now' as string | DateTime },
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePickerPopover
|
||||
value={value}
|
||||
timeZone="browser"
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
));
|
@ -1,122 +0,0 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// Components
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import { TimePickerInput } from './TimePickerInput';
|
||||
import { rawToTimeRange } from './time';
|
||||
|
||||
// Types
|
||||
import { DateTime, TimeRange, TimeZone } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
timeZone?: TimeZone;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
from: DateTime | string;
|
||||
to: DateTime | string;
|
||||
isFromInputValid: boolean;
|
||||
isToInputValid: boolean;
|
||||
}
|
||||
|
||||
export class TimePickerPopover extends Component<Props, State> {
|
||||
static popoverClassName = 'time-picker-popover';
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
from: props.value.raw.from,
|
||||
to: props.value.raw.to,
|
||||
isFromInputValid: true,
|
||||
isToInputValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
onFromInputChanged = (value: string, valid: boolean) => {
|
||||
this.setState({ from: value, isFromInputValid: valid });
|
||||
};
|
||||
|
||||
onToInputChanged = (value: string, valid: boolean) => {
|
||||
this.setState({ to: value, isToInputValid: valid });
|
||||
};
|
||||
|
||||
onFromCalendarChanged = (value: DateTime) => {
|
||||
this.setState({ from: value });
|
||||
};
|
||||
|
||||
onToCalendarChanged = (value: DateTime) => {
|
||||
value.set('h', 23);
|
||||
value.set('m', 59);
|
||||
value.set('s', 59);
|
||||
this.setState({ to: value });
|
||||
};
|
||||
|
||||
onApplyClick = () => {
|
||||
const { onChange, timeZone } = this.props;
|
||||
const { from, to } = this.state;
|
||||
|
||||
onChange(rawToTimeRange({ from, to }, timeZone));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { timeZone } = this.props;
|
||||
const { isFromInputValid, isToInputValid, from, to } = this.state;
|
||||
|
||||
const isValid = isFromInputValid && isToInputValid;
|
||||
|
||||
return (
|
||||
<div className={TimePickerPopover.popoverClassName}>
|
||||
<div className="time-picker-popover-body">
|
||||
<div className="time-picker-popover-body-custom-ranges">
|
||||
<div className="time-picker-popover-body-custom-ranges-input">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">From</label>
|
||||
<TimePickerInput
|
||||
roundup={false}
|
||||
timeZone={timeZone}
|
||||
value={from}
|
||||
onChange={this.onFromInputChanged}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar
|
||||
timeZone={timeZone}
|
||||
roundup={false}
|
||||
value={from}
|
||||
onChange={this.onFromCalendarChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-body-custom-ranges">
|
||||
<div className="time-picker-popover-body-custom-ranges-input">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">To</label>
|
||||
<TimePickerInput
|
||||
roundup={true}
|
||||
timeZone={timeZone}
|
||||
value={to}
|
||||
onChange={this.onToInputChanged}
|
||||
tabIndex={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar roundup={true} timeZone={timeZone} value={to} onChange={this.onToCalendarChanged} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-footer">
|
||||
<button type="submit" className="btn btn-success" disabled={!isValid} onClick={this.onApplyClick}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
.time-picker {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.time-picker-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-utc {
|
||||
color: $orange;
|
||||
font-size: 75%;
|
||||
padding: 3px;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
margin-left: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.time-picker-popover {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: 1px solid $popover-border-color;
|
||||
border-radius: $border-radius;
|
||||
background: $popover-bg;
|
||||
color: $popover-color;
|
||||
box-shadow: $popover-shadow;
|
||||
position: absolute;
|
||||
z-index: $zindex-dropdown;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
top: 41px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.time-picker-popover-body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
padding: $space-md;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.time-picker-popover-title {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.time-picker-popover-body-custom-ranges:first-child {
|
||||
margin-right: $space-md;
|
||||
}
|
||||
|
||||
.time-picker-popover-body-custom-ranges-input {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: $space-sm;
|
||||
|
||||
.time-picker-input-error {
|
||||
box-shadow: inset 0 0px 5px $red;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
padding: $space-md;
|
||||
}
|
||||
|
||||
.time-picker-popover-header {
|
||||
background: $popover-header-bg;
|
||||
padding: $space-sm;
|
||||
}
|
||||
|
||||
.time-picker-input {
|
||||
max-width: 170px;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label {
|
||||
line-height: 31px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__arrow {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
$arrowPaddingToBorder: 7px;
|
||||
$arrowPadding: $arrowPaddingToBorder * 3;
|
||||
|
||||
.react-calendar__navigation__next-button {
|
||||
padding-left: $arrowPadding;
|
||||
padding-right: $arrowPaddingToBorder;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__prev-button {
|
||||
padding-left: $arrowPaddingToBorder;
|
||||
padding-right: $arrowPadding;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth abbr {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.time-picker-calendar {
|
||||
border: 1px solid $input-border-color;
|
||||
color: $black;
|
||||
width: 260px;
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
color: $input-color;
|
||||
background-color: $input-label-bg;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
background-color: $input-bg;
|
||||
text-align: center;
|
||||
|
||||
abbr {
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
color: $orange;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
display: block;
|
||||
padding: 4px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-calendar-tile {
|
||||
color: $text-color;
|
||||
background-color: inherit;
|
||||
line-height: 26px;
|
||||
font-size: $font-size-md;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $panel-editor-viz-item-shadow-hover;
|
||||
background: $panel-editor-viz-item-bg-hover;
|
||||
border: $panel-editor-viz-item-border-hover;
|
||||
color: $text-color-strong;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days {
|
||||
background-color: $calendar-bg-days;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
background-color: $calendar-bg-now;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation > button:focus,
|
||||
.time-picker-calendar-tile:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active,
|
||||
.react-calendar__tile--active:hover {
|
||||
color: $white;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
background: linear-gradient(0deg, $blue-base, $blue-shade);
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-custom-range-label {
|
||||
padding-right: $space-xs;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.time-picker-popover {
|
||||
.time-picker-popover-title {
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
|
||||
.time-picker-popover-body {
|
||||
padding: $space-sm;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.time-picker-popover-body-custom-ranges:first-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: $space-sm;
|
||||
}
|
||||
|
||||
.time-picker-popover-footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
margin-top: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-calendar {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-button-select {
|
||||
.select-button-value {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
display: inline-block;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// special rules for when within explore toolbar in split
|
||||
.explore-toolbar.splitted {
|
||||
.time-picker-button-select {
|
||||
.select-button-value {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1545px) {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-timepicker-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
// this is to align popover position with explore ( .explore-toolbar-content-item class)
|
||||
padding: 2px 2px;
|
||||
|
||||
.gf-form-select-box__menu {
|
||||
right: 0;
|
||||
left: unset;
|
||||
}
|
||||
}
|
@ -0,0 +1,818 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TimePicker renders buttons correctly 1`] = `
|
||||
<div
|
||||
className="css-16ba5ut"
|
||||
>
|
||||
<div
|
||||
className="css-vyoujf"
|
||||
>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--tight"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Component
|
||||
content={
|
||||
<TimePickerTooltip
|
||||
timeRange={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
aria-label="TimePicker Open Button"
|
||||
className="btn navbar-button navbar-button--zoom"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-clock-o fa-fw"
|
||||
/>
|
||||
<Component
|
||||
onChange={[Function]}
|
||||
onMoveBackward={[Function]}
|
||||
onMoveForward={[Function]}
|
||||
onZoom={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"background": Object {
|
||||
"dropdown": "#1f1f20",
|
||||
"pageHeader": "linear-gradient(90deg, #292a2d, #000000)",
|
||||
"scrollbar": "#343436",
|
||||
"scrollbar2": "#343436",
|
||||
},
|
||||
"border": Object {
|
||||
"radius": Object {
|
||||
"lg": "5px",
|
||||
"md": "3px",
|
||||
"sm": "2px",
|
||||
},
|
||||
"width": Object {
|
||||
"sm": "1px",
|
||||
},
|
||||
},
|
||||
"breakpoints": Object {
|
||||
"lg": "992px",
|
||||
"md": "769px",
|
||||
"sm": "544px",
|
||||
"xl": "1200px",
|
||||
"xs": "0",
|
||||
},
|
||||
"colors": Object {
|
||||
"black": "#000000",
|
||||
"blue": "#33b5e5",
|
||||
"blue77": "#1f60c4",
|
||||
"blue85": "#3274d9",
|
||||
"blue95": "#5794f2",
|
||||
"blueBase": "#3274d9",
|
||||
"blueFaint": "#041126",
|
||||
"blueLight": "#5794f2",
|
||||
"blueShade": "#1f60c4",
|
||||
"body": "#d8d9da",
|
||||
"bodyBg": "#161719",
|
||||
"brandDanger": "#e02f44",
|
||||
"brandPrimary": "#eb7b18",
|
||||
"brandSuccess": "#299c46",
|
||||
"brandWarning": "#eb7b18",
|
||||
"critical": "#e02f44",
|
||||
"dark1": "#141414",
|
||||
"dark10": "#424345",
|
||||
"dark2": "#161719",
|
||||
"dark3": "#1f1f20",
|
||||
"dark4": "#212124",
|
||||
"dark5": "#222426",
|
||||
"dark6": "#262628",
|
||||
"dark7": "#292a2d",
|
||||
"dark8": "#2f2f32",
|
||||
"dark9": "#343436",
|
||||
"formCheckboxBg": "#222426",
|
||||
"formCheckboxBgChecked": "#5794f2",
|
||||
"formCheckboxBgCheckedHover": "#3274d9",
|
||||
"formCheckboxCheckmark": "#343b40",
|
||||
"formDescription": "#9fa7b3",
|
||||
"formFocusOutline": "#1f60c4",
|
||||
"formInputBg": "#202226",
|
||||
"formInputBgDisabled": "#141619",
|
||||
"formInputBorder": "#343b40",
|
||||
"formInputBorderActive": "#5794f2",
|
||||
"formInputBorderHover": "#464c54",
|
||||
"formInputBorderInvalid": "#e02f44",
|
||||
"formInputDisabledText": "#9fa7b3",
|
||||
"formInputText": "#c7d0d9",
|
||||
"formInputTextStrong": "#c7d0d9",
|
||||
"formInputTextWhite": "#ffffff",
|
||||
"formLabel": "#9fa7b3",
|
||||
"formLegend": "#c7d0d9",
|
||||
"formSwitchBg": "#343b40",
|
||||
"formSwitchBgActive": "#5794f2",
|
||||
"formSwitchBgActiveHover": "#3274d9",
|
||||
"formSwitchBgDisabled": "#343b40",
|
||||
"formSwitchBgHover": "#464c54",
|
||||
"formSwitchDot": "#202226",
|
||||
"formValidationMessageBg": "#e02f44",
|
||||
"formValidationMessageText": "#ffffff",
|
||||
"gray05": "#0b0c0e",
|
||||
"gray1": "#555555",
|
||||
"gray10": "#141619",
|
||||
"gray15": "#202226",
|
||||
"gray2": "#8e8e8e",
|
||||
"gray25": "#343b40",
|
||||
"gray3": "#b3b3b3",
|
||||
"gray33": "#464c54",
|
||||
"gray4": "#d8d9da",
|
||||
"gray5": "#ececec",
|
||||
"gray6": "#f4f5f8",
|
||||
"gray7": "#fbfbfb",
|
||||
"gray70": "#9fa7b3",
|
||||
"gray85": "#c7d0d9",
|
||||
"gray95": "#e9edf2",
|
||||
"gray98": "#f7f8fa",
|
||||
"grayBlue": "#212327",
|
||||
"greenBase": "#299c46",
|
||||
"greenShade": "#23843b",
|
||||
"headingColor": "#d8d9da",
|
||||
"inputBlack": "#09090b",
|
||||
"link": "#d8d9da",
|
||||
"linkDisabled": "#8e8e8e",
|
||||
"linkExternal": "#33b5e5",
|
||||
"linkHover": "#ffffff",
|
||||
"online": "#299c46",
|
||||
"orange": "#eb7b18",
|
||||
"orangeDark": "#ff780a",
|
||||
"pageBg": "#161719",
|
||||
"pageHeaderBorder": "#343436",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
"queryOrange": "#eb7b18",
|
||||
"queryPurple": "#fe85fc",
|
||||
"queryRed": "#e02f44",
|
||||
"red": "#d44a3a",
|
||||
"red88": "#e02f44",
|
||||
"redBase": "#e02f44",
|
||||
"redShade": "#c4162a",
|
||||
"text": "#d8d9da",
|
||||
"textEmphasis": "#ececec",
|
||||
"textFaint": "#222426",
|
||||
"textStrong": "#ffffff",
|
||||
"textWeak": "#8e8e8e",
|
||||
"variable": "#32d1df",
|
||||
"warn": "#f79520",
|
||||
"white": "#ffffff",
|
||||
"yellow": "#ecbb13",
|
||||
},
|
||||
"height": Object {
|
||||
"lg": "48px",
|
||||
"md": "32px",
|
||||
"sm": "24px",
|
||||
},
|
||||
"isDark": true,
|
||||
"isLight": false,
|
||||
"name": "Grafana Dark",
|
||||
"panelHeaderHeight": 28,
|
||||
"panelPadding": 8,
|
||||
"shadow": Object {
|
||||
"pageHeader": "inset 0px -4px 14px #1f1f20",
|
||||
},
|
||||
"spacing": Object {
|
||||
"d": "14px",
|
||||
"formButtonHeight": 32,
|
||||
"formFieldsetMargin": "16px",
|
||||
"formInputAffixPaddingHorizontal": "4px",
|
||||
"formInputHeight": "32px",
|
||||
"formInputMargin": "16px",
|
||||
"formInputPaddingHorizontal": "8px",
|
||||
"formLabelMargin": "0 0 4px 0",
|
||||
"formLabelPadding": "0 0 0 2px",
|
||||
"formLegendMargin": "0 0 16px 0",
|
||||
"formMargin": "32px",
|
||||
"formSpacingBase": 8,
|
||||
"formValidationMessageMargin": "4px 0 0 0",
|
||||
"formValidationMessagePadding": "4px 8px",
|
||||
"gutter": "30px",
|
||||
"insetSquishMd": "4px 8px",
|
||||
"lg": "24px",
|
||||
"md": "16px",
|
||||
"sm": "8px",
|
||||
"xl": "32px",
|
||||
"xs": "4px",
|
||||
"xxs": "2px",
|
||||
},
|
||||
"type": "dark",
|
||||
"typography": Object {
|
||||
"fontFamily": Object {
|
||||
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
||||
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
"heading": Object {
|
||||
"h1": "28px",
|
||||
"h2": "24px",
|
||||
"h3": "21px",
|
||||
"h4": "18px",
|
||||
"h5": "16px",
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
"link": Object {
|
||||
"decoration": "none",
|
||||
"hoverDecoration": "none",
|
||||
},
|
||||
"size": Object {
|
||||
"base": "14px",
|
||||
"lg": "18px",
|
||||
"md": "14px",
|
||||
"sm": "12px",
|
||||
"xs": "10px",
|
||||
},
|
||||
"weight": Object {
|
||||
"bold": 600,
|
||||
"light": 300,
|
||||
"regular": 400,
|
||||
"semibold": 500,
|
||||
},
|
||||
},
|
||||
"zIndex": Object {
|
||||
"dropdown": "1000",
|
||||
"modal": "1050",
|
||||
"modalBackdrop": "1040",
|
||||
"navbarFixed": "1020",
|
||||
"sidemenu": "1025",
|
||||
"tooltip": "1030",
|
||||
"typeahead": "1060",
|
||||
},
|
||||
}
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="css-6x26ye"
|
||||
>
|
||||
<i
|
||||
className="fa fa-caret-down fa-fw"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</Component>
|
||||
</div>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--tight"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
/>
|
||||
</button>
|
||||
<Component
|
||||
content={[Function]}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--zoom"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-search-minus"
|
||||
/>
|
||||
</button>
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TimePicker renders content correctly after beeing open 1`] = `
|
||||
<div
|
||||
className="css-16ba5ut"
|
||||
>
|
||||
<div
|
||||
className="css-vyoujf"
|
||||
>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--tight"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Component
|
||||
content={
|
||||
<TimePickerTooltip
|
||||
timeRange={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
aria-label="TimePicker Open Button"
|
||||
className="btn navbar-button navbar-button--zoom"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-clock-o fa-fw"
|
||||
/>
|
||||
<Component
|
||||
onChange={[Function]}
|
||||
onMoveBackward={[Function]}
|
||||
onMoveForward={[Function]}
|
||||
onZoom={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"background": Object {
|
||||
"dropdown": "#1f1f20",
|
||||
"pageHeader": "linear-gradient(90deg, #292a2d, #000000)",
|
||||
"scrollbar": "#343436",
|
||||
"scrollbar2": "#343436",
|
||||
},
|
||||
"border": Object {
|
||||
"radius": Object {
|
||||
"lg": "5px",
|
||||
"md": "3px",
|
||||
"sm": "2px",
|
||||
},
|
||||
"width": Object {
|
||||
"sm": "1px",
|
||||
},
|
||||
},
|
||||
"breakpoints": Object {
|
||||
"lg": "992px",
|
||||
"md": "769px",
|
||||
"sm": "544px",
|
||||
"xl": "1200px",
|
||||
"xs": "0",
|
||||
},
|
||||
"colors": Object {
|
||||
"black": "#000000",
|
||||
"blue": "#33b5e5",
|
||||
"blue77": "#1f60c4",
|
||||
"blue85": "#3274d9",
|
||||
"blue95": "#5794f2",
|
||||
"blueBase": "#3274d9",
|
||||
"blueFaint": "#041126",
|
||||
"blueLight": "#5794f2",
|
||||
"blueShade": "#1f60c4",
|
||||
"body": "#d8d9da",
|
||||
"bodyBg": "#161719",
|
||||
"brandDanger": "#e02f44",
|
||||
"brandPrimary": "#eb7b18",
|
||||
"brandSuccess": "#299c46",
|
||||
"brandWarning": "#eb7b18",
|
||||
"critical": "#e02f44",
|
||||
"dark1": "#141414",
|
||||
"dark10": "#424345",
|
||||
"dark2": "#161719",
|
||||
"dark3": "#1f1f20",
|
||||
"dark4": "#212124",
|
||||
"dark5": "#222426",
|
||||
"dark6": "#262628",
|
||||
"dark7": "#292a2d",
|
||||
"dark8": "#2f2f32",
|
||||
"dark9": "#343436",
|
||||
"formCheckboxBg": "#222426",
|
||||
"formCheckboxBgChecked": "#5794f2",
|
||||
"formCheckboxBgCheckedHover": "#3274d9",
|
||||
"formCheckboxCheckmark": "#343b40",
|
||||
"formDescription": "#9fa7b3",
|
||||
"formFocusOutline": "#1f60c4",
|
||||
"formInputBg": "#202226",
|
||||
"formInputBgDisabled": "#141619",
|
||||
"formInputBorder": "#343b40",
|
||||
"formInputBorderActive": "#5794f2",
|
||||
"formInputBorderHover": "#464c54",
|
||||
"formInputBorderInvalid": "#e02f44",
|
||||
"formInputDisabledText": "#9fa7b3",
|
||||
"formInputText": "#c7d0d9",
|
||||
"formInputTextStrong": "#c7d0d9",
|
||||
"formInputTextWhite": "#ffffff",
|
||||
"formLabel": "#9fa7b3",
|
||||
"formLegend": "#c7d0d9",
|
||||
"formSwitchBg": "#343b40",
|
||||
"formSwitchBgActive": "#5794f2",
|
||||
"formSwitchBgActiveHover": "#3274d9",
|
||||
"formSwitchBgDisabled": "#343b40",
|
||||
"formSwitchBgHover": "#464c54",
|
||||
"formSwitchDot": "#202226",
|
||||
"formValidationMessageBg": "#e02f44",
|
||||
"formValidationMessageText": "#ffffff",
|
||||
"gray05": "#0b0c0e",
|
||||
"gray1": "#555555",
|
||||
"gray10": "#141619",
|
||||
"gray15": "#202226",
|
||||
"gray2": "#8e8e8e",
|
||||
"gray25": "#343b40",
|
||||
"gray3": "#b3b3b3",
|
||||
"gray33": "#464c54",
|
||||
"gray4": "#d8d9da",
|
||||
"gray5": "#ececec",
|
||||
"gray6": "#f4f5f8",
|
||||
"gray7": "#fbfbfb",
|
||||
"gray70": "#9fa7b3",
|
||||
"gray85": "#c7d0d9",
|
||||
"gray95": "#e9edf2",
|
||||
"gray98": "#f7f8fa",
|
||||
"grayBlue": "#212327",
|
||||
"greenBase": "#299c46",
|
||||
"greenShade": "#23843b",
|
||||
"headingColor": "#d8d9da",
|
||||
"inputBlack": "#09090b",
|
||||
"link": "#d8d9da",
|
||||
"linkDisabled": "#8e8e8e",
|
||||
"linkExternal": "#33b5e5",
|
||||
"linkHover": "#ffffff",
|
||||
"online": "#299c46",
|
||||
"orange": "#eb7b18",
|
||||
"orangeDark": "#ff780a",
|
||||
"pageBg": "#161719",
|
||||
"pageHeaderBorder": "#343436",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
"queryOrange": "#eb7b18",
|
||||
"queryPurple": "#fe85fc",
|
||||
"queryRed": "#e02f44",
|
||||
"red": "#d44a3a",
|
||||
"red88": "#e02f44",
|
||||
"redBase": "#e02f44",
|
||||
"redShade": "#c4162a",
|
||||
"text": "#d8d9da",
|
||||
"textEmphasis": "#ececec",
|
||||
"textFaint": "#222426",
|
||||
"textStrong": "#ffffff",
|
||||
"textWeak": "#8e8e8e",
|
||||
"variable": "#32d1df",
|
||||
"warn": "#f79520",
|
||||
"white": "#ffffff",
|
||||
"yellow": "#ecbb13",
|
||||
},
|
||||
"height": Object {
|
||||
"lg": "48px",
|
||||
"md": "32px",
|
||||
"sm": "24px",
|
||||
},
|
||||
"isDark": true,
|
||||
"isLight": false,
|
||||
"name": "Grafana Dark",
|
||||
"panelHeaderHeight": 28,
|
||||
"panelPadding": 8,
|
||||
"shadow": Object {
|
||||
"pageHeader": "inset 0px -4px 14px #1f1f20",
|
||||
},
|
||||
"spacing": Object {
|
||||
"d": "14px",
|
||||
"formButtonHeight": 32,
|
||||
"formFieldsetMargin": "16px",
|
||||
"formInputAffixPaddingHorizontal": "4px",
|
||||
"formInputHeight": "32px",
|
||||
"formInputMargin": "16px",
|
||||
"formInputPaddingHorizontal": "8px",
|
||||
"formLabelMargin": "0 0 4px 0",
|
||||
"formLabelPadding": "0 0 0 2px",
|
||||
"formLegendMargin": "0 0 16px 0",
|
||||
"formMargin": "32px",
|
||||
"formSpacingBase": 8,
|
||||
"formValidationMessageMargin": "4px 0 0 0",
|
||||
"formValidationMessagePadding": "4px 8px",
|
||||
"gutter": "30px",
|
||||
"insetSquishMd": "4px 8px",
|
||||
"lg": "24px",
|
||||
"md": "16px",
|
||||
"sm": "8px",
|
||||
"xl": "32px",
|
||||
"xs": "4px",
|
||||
"xxs": "2px",
|
||||
},
|
||||
"type": "dark",
|
||||
"typography": Object {
|
||||
"fontFamily": Object {
|
||||
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
||||
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
"heading": Object {
|
||||
"h1": "28px",
|
||||
"h2": "24px",
|
||||
"h3": "21px",
|
||||
"h4": "18px",
|
||||
"h5": "16px",
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
"link": Object {
|
||||
"decoration": "none",
|
||||
"hoverDecoration": "none",
|
||||
},
|
||||
"size": Object {
|
||||
"base": "14px",
|
||||
"lg": "18px",
|
||||
"md": "14px",
|
||||
"sm": "12px",
|
||||
"xs": "10px",
|
||||
},
|
||||
"weight": Object {
|
||||
"bold": 600,
|
||||
"light": 300,
|
||||
"regular": 400,
|
||||
"semibold": 500,
|
||||
},
|
||||
},
|
||||
"zIndex": Object {
|
||||
"dropdown": "1000",
|
||||
"modal": "1050",
|
||||
"modalBackdrop": "1040",
|
||||
"navbarFixed": "1020",
|
||||
"sidemenu": "1025",
|
||||
"tooltip": "1030",
|
||||
"typeahead": "1060",
|
||||
},
|
||||
}
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="css-6x26ye"
|
||||
>
|
||||
<i
|
||||
className="fa fa-caret-up fa-fw"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</Component>
|
||||
<ClickOutsideWrapper
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Component
|
||||
onChange={[Function]}
|
||||
otherOptions={
|
||||
Array [
|
||||
Object {
|
||||
"display": "Yesterday",
|
||||
"from": "now-1d/d",
|
||||
"section": 3,
|
||||
"to": "now-1d/d",
|
||||
},
|
||||
Object {
|
||||
"display": "Day before yesterday",
|
||||
"from": "now-2d/d",
|
||||
"section": 3,
|
||||
"to": "now-2d/d",
|
||||
},
|
||||
Object {
|
||||
"display": "This day last week",
|
||||
"from": "now-7d/d",
|
||||
"section": 3,
|
||||
"to": "now-7d/d",
|
||||
},
|
||||
Object {
|
||||
"display": "Previous week",
|
||||
"from": "now-1w/w",
|
||||
"section": 3,
|
||||
"to": "now-1w/w",
|
||||
},
|
||||
Object {
|
||||
"display": "Previous month",
|
||||
"from": "now-1M/M",
|
||||
"section": 3,
|
||||
"to": "now-1M/M",
|
||||
},
|
||||
Object {
|
||||
"display": "Previous year",
|
||||
"from": "now-1y/y",
|
||||
"section": 3,
|
||||
"to": "now-1y/y",
|
||||
},
|
||||
Object {
|
||||
"display": "Today",
|
||||
"from": "now/d",
|
||||
"section": 3,
|
||||
"to": "now/d",
|
||||
},
|
||||
Object {
|
||||
"display": "Today so far",
|
||||
"from": "now/d",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "This week",
|
||||
"from": "now/w",
|
||||
"section": 3,
|
||||
"to": "now/w",
|
||||
},
|
||||
Object {
|
||||
"display": "This week so far",
|
||||
"from": "now/w",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "This month",
|
||||
"from": "now/M",
|
||||
"section": 3,
|
||||
"to": "now/M",
|
||||
},
|
||||
Object {
|
||||
"display": "This month so far",
|
||||
"from": "now/M",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "This year",
|
||||
"from": "now/y",
|
||||
"section": 3,
|
||||
"to": "now/y",
|
||||
},
|
||||
Object {
|
||||
"display": "This year so far",
|
||||
"from": "now/y",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
]
|
||||
}
|
||||
quickOptions={
|
||||
Array [
|
||||
Object {
|
||||
"display": "Last 5 minutes",
|
||||
"from": "now-5m",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 15 minutes",
|
||||
"from": "now-15m",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 30 minutes",
|
||||
"from": "now-30m",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 1 hour",
|
||||
"from": "now-1h",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 3 hours",
|
||||
"from": "now-3h",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 6 hours",
|
||||
"from": "now-6h",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 12 hours",
|
||||
"from": "now-12h",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 24 hours",
|
||||
"from": "now-24h",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 2 days",
|
||||
"from": "now-2d",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 7 days",
|
||||
"from": "now-7d",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 30 days",
|
||||
"from": "now-30d",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 90 days",
|
||||
"from": "now-90d",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 6 months",
|
||||
"from": "now-6M",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 1 year",
|
||||
"from": "now-1y",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 2 years",
|
||||
"from": "now-2y",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
Object {
|
||||
"display": "Last 5 years",
|
||||
"from": "now-5y",
|
||||
"section": 3,
|
||||
"to": "now",
|
||||
},
|
||||
]
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"raw": Object {
|
||||
"from": "2019-12-17T07:48:27.433Z",
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
},
|
||||
"to": "2019-12-18T07:48:27.433Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ClickOutsideWrapper>
|
||||
</div>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--tight"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
/>
|
||||
</button>
|
||||
<Component
|
||||
content={[Function]}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className="btn navbar-button navbar-button--zoom"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-search-minus"
|
||||
/>
|
||||
</button>
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -12,6 +12,5 @@
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'TimePicker/TimeOfDayPicker';
|
||||
@import 'TimePicker/TimePicker';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
|
@ -0,0 +1,37 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import store from '../../store';
|
||||
|
||||
export interface Props<T> {
|
||||
storageKey: string;
|
||||
defaultValue?: T;
|
||||
children: (value: T, onSaveToStore: (value: T) => void) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface State<T> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
export class LocalStorageValueProvider<T> extends PureComponent<Props<T>, State<T>> {
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
|
||||
const { storageKey, defaultValue } = props;
|
||||
|
||||
this.state = {
|
||||
value: store.getObject(storageKey, defaultValue),
|
||||
};
|
||||
}
|
||||
|
||||
onSaveToStore = (value: T) => {
|
||||
const { storageKey } = this.props;
|
||||
store.setObject(storageKey, value);
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
return <>{children(value, this.onSaveToStore)}</>;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { LocalStorageValueProvider } from './LocalStorageValueProvider';
|
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
|
||||
import { TimeRange, isDateTime } from '@grafana/data';
|
||||
import { Props as TimePickerProps, TimePicker } from '@grafana/ui/src/components/TimePicker/TimePicker';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';
|
||||
|
||||
interface Props extends Omit<TimePickerProps, 'history' | 'theme'> {}
|
||||
|
||||
export const TimePickerWithHistory: React.FC<Props> = props => {
|
||||
return (
|
||||
<LocalStorageValueProvider<TimeRange[]> storageKey={LOCAL_STORAGE_KEY} defaultValue={[]}>
|
||||
{(values, onSaveToStore) => {
|
||||
return (
|
||||
<TimePicker
|
||||
{...props}
|
||||
history={values}
|
||||
onChange={value => {
|
||||
onAppendToHistory(value, values, onSaveToStore);
|
||||
props.onChange(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</LocalStorageValueProvider>
|
||||
);
|
||||
};
|
||||
function onAppendToHistory(toAppend: TimeRange, values: TimeRange[], onSaveToStore: (values: TimeRange[]) => void) {
|
||||
if (!isAbsolute(toAppend)) {
|
||||
return;
|
||||
}
|
||||
const toStore = limit([toAppend, ...values]);
|
||||
onSaveToStore(toStore);
|
||||
}
|
||||
|
||||
function isAbsolute(value: TimeRange): boolean {
|
||||
return isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||
}
|
||||
|
||||
function limit(value: TimeRange[]): TimeRange[] {
|
||||
return value.slice(0, 4);
|
||||
}
|
@ -1,30 +1,40 @@
|
||||
// Libaries
|
||||
import React, { Component } from 'react';
|
||||
import { dateMath } from '@grafana/data';
|
||||
import { dateMath, GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../../state';
|
||||
import { LocationState, CoreEvents } from 'app/types';
|
||||
import { TimeRange, TimeOption, RawTimeRange } from '@grafana/data';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
// State
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Components
|
||||
import { TimePicker, RefreshPicker } from '@grafana/ui';
|
||||
import { RefreshPicker, withTheme, stylesFactory, Themeable } from '@grafana/ui';
|
||||
import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory';
|
||||
|
||||
// Utils & Services
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
|
||||
|
||||
export interface Props {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 2px 2px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export interface Props extends Themeable {
|
||||
$injector: any;
|
||||
dashboard: DashboardModel;
|
||||
updateLocation: typeof updateLocation;
|
||||
location: LocationState;
|
||||
}
|
||||
|
||||
export class DashNavTimeControls extends Component<Props> {
|
||||
class UnthemedDashNavTimeControls extends Component<Props> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
$rootScope = this.props.$injector.get('$rootScope');
|
||||
|
||||
@ -83,37 +93,22 @@ export class DashNavTimeControls extends Component<Props> {
|
||||
this.$rootScope.appEvent(CoreEvents.zoomOut, 2);
|
||||
};
|
||||
|
||||
setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => {
|
||||
return timeOptions.map(option => {
|
||||
if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) {
|
||||
return {
|
||||
...option,
|
||||
active: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...option,
|
||||
active: false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { dashboard, theme } = this.props;
|
||||
const intervals = dashboard.timepicker.refresh_intervals;
|
||||
const timePickerValue = this.timeSrv.timeRange();
|
||||
const timeZone = dashboard.getTimezone();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className="dashboard-timepicker-wrapper">
|
||||
<TimePicker
|
||||
<div className={styles.container}>
|
||||
<TimePickerWithHistory
|
||||
value={timePickerValue}
|
||||
onChange={this.onChangeTimePicker}
|
||||
timeZone={timeZone}
|
||||
onMoveBackward={this.onMoveBack}
|
||||
onMoveForward={this.onMoveForward}
|
||||
onZoom={this.onZoom}
|
||||
selectOptions={this.setActiveTimeOption(defaultSelectOptions, timePickerValue.raw)}
|
||||
/>
|
||||
<RefreshPicker
|
||||
onIntervalChanged={this.onChangeRefreshInterval}
|
||||
@ -126,3 +121,5 @@ export class DashNavTimeControls extends Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashNavTimeControls = withTheme(UnthemedDashNavTimeControls);
|
||||
|
@ -3,16 +3,15 @@ import React, { Component } from 'react';
|
||||
|
||||
// Types
|
||||
import { ExploreId } from 'app/types';
|
||||
import { TimeRange, TimeOption, TimeZone, RawTimeRange, dateTimeForTimeZone } from '@grafana/data';
|
||||
import { TimeRange, TimeZone, RawTimeRange, dateTimeForTimeZone } from '@grafana/data';
|
||||
|
||||
// State
|
||||
|
||||
// Components
|
||||
import { TimePicker } from '@grafana/ui';
|
||||
import { TimeSyncButton } from './TimeSyncButton';
|
||||
import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory';
|
||||
|
||||
// Utils & Services
|
||||
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
|
||||
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
|
||||
|
||||
export interface Props {
|
||||
@ -56,35 +55,25 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
onChangeTime(nextTimeRange);
|
||||
};
|
||||
|
||||
setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => {
|
||||
return timeOptions.map(option => {
|
||||
if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) {
|
||||
return {
|
||||
...option,
|
||||
active: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...option,
|
||||
active: false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText } = this.props;
|
||||
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
|
||||
const timePickerCommonProps = {
|
||||
value: range,
|
||||
onChange: this.onChangeTimePicker,
|
||||
timeZone,
|
||||
onMoveBackward: this.onMoveBack,
|
||||
onMoveForward: this.onMoveForward,
|
||||
onZoom: this.onZoom,
|
||||
selectOptions: this.setActiveTimeOption(defaultSelectOptions, range.raw),
|
||||
hideText,
|
||||
};
|
||||
|
||||
return <TimePicker {...timePickerCommonProps} timeSyncButton={timeSyncButton} isSynced={syncedTimes} />;
|
||||
return (
|
||||
<TimePickerWithHistory
|
||||
{...timePickerCommonProps}
|
||||
timeSyncButton={timeSyncButton}
|
||||
isSynced={syncedTimes}
|
||||
onChange={this.onChangeTimePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user