mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore & Dashboard: New Refresh picker (#16505)
* Added RefreshButton
* Added RefreshSelect
* Added RefreshSelectButton
* Added RefreshPicker
* Removed the magic string Paused
* Minor style changes and using Off instead of Pause
* Added HeadlessSelect
* Added HeadlessSelect story
* Added SelectButton
* Removed RefreshSelectButton
* Added TimePicker and moved ClickOutsideWrapper to ui/components
* Added TimePickerPopOver
* Added react-calendar
* Missed yarn lock file
* Added inputs to popover
* Added TimePicker and RefreshPicker to DashNav
* Moved TimePicker and RefreshPicker to app/core
* Added react-calendar to app and removed from ui/components
* Fixed PopOver onClick
* Moved everything back to ui components because of typings problems
* Exporing RefreshPicker and TimePicker
* Added Apply and inputs
* Added typings
* Added TimePickerInput and logic
* Fixed parsing of string to Moments
* Fixed range string
* Styling and connecting the calendars and inputs
* Changed Calendar styling
* Added backward forward and zoom
* Fixed responsive styles
* Moved TimePicker and RefreshPicker into app core
* Renamed menuIsOpen to isOpen
* Changed from className={} to className=""
* Moved Popover to TimePickerOptionGroup
* Renamed all PopOver to Popover
* Renamed popOver to popover and some minor refactorings
* Renamed files with git mv
* Added ButtonSelect and refactored RefreshPicker
* Refactored TimePicker to use new ButtonSelect
* Removed HeadlessSelect as suggested
* fix: Fix typings and misc errors after rebase
* wip: Enable time picker on dashboard and add tooltip
* Merge branch 'master' into hugoh/new-timepicker-and-unified-component
# Conflicts:
# packages/grafana-ui/package.json
# packages/grafana-ui/src/components/Input/Input.test.tsx
# packages/grafana-ui/src/components/Input/Input.tsx
# packages/grafana-ui/src/utils/validate.ts
# public/app/features/dashboard/panel_editor/QueryOptions.tsx
# yarn.lock
* fix: Snapshot update
* Move TimePicker default options into the TimePicker as statics, pass the tooltipContent down the line when wanted and wrap the button in a tooltip element
* fix: Override internal state prop if we provide one in a prop
* Updated snapshots
* Let dashnav control refreshPicker state
* feat: Add a stringToMs function
* wip: RefreshPicker
* wip: Move RefreshPicker to @grafana/ui
* wip: Move TimePicker to @grafana/ui
* wip: Remove comments
* wip: Add refreshPicker to explore
* wip: Use default intervals if the prop is missing
* wip: Nicer way of setting defaults
* fix: Control the select component
* wip: Add onMoveForward/onMoveBack
* Remove code related to the new time picker and refresh picker from dashnav
* Fix: Typings after merge
* chore: Minor fix after merge
* chore: Remove _.map usage
* chore: Moved refresh-picker logic out of the refresh picker since it will work a little differently in explore and dashboards until we have replaced the TimeSrv
* feat: Add an Interval component to @grafana/ui
* chore: Remove intervalId from redux state and move setInterval logic from ExploreToolbar to its own Interval component
* feat: Add refreshInterval to Explore's URL state
* feat: Pick up refreshInterval from url on page load
* fix: Set default refreshInterval when no value can be retained from URL
* fix: Update test initial state with refreshInterval
* fix: Handle URLs before RefreshPicker
* fix: Move RefreshInterval to url position 3 since the segments can take multiple positions
* fix: A better way of detecting urls without RefreshInterval in Explore
* chore: Some Explore typings
* fix: Attach refresh picker to interval picker
* chore: Sass fix for refresh button border radius
* fix: Remove refreshInterval from URL
* fix: Intervals now start when previous interval is finished
* fix: Use clearTimeout instead of clearInterval
* fix: Make sure there's a delay set before adding a timeout when we have slow explore queries
* wip: Add refresh picker to dashboard
* feat: Add util for removing keys with empty values
* feat: RefreshPicker in dashboards and tmp rem out old RefreshPicker
* fix: Remove the jumpy:ness in the refreshpicker
* Changed placement and made it hide when your in dashboard settings
* chore: Move logic related to refresh picker out of DashNav to its own component
* feat: Add tooltip to refreshpicker
* fix: Fix bug with refreshpicker not updating when setting to 'off'
* fix: Make it possible to override refresh intervals using the dashboard intervals
* chore: Change name of Interval to SetInterval to align with ecmascripts naming since its basically the same but declarative and async
* fix: Use default intervals when auto refresh is empty in dashboard settings
* fix: Hide time/interval picker when hidden is true on the model, such as on the home dashboard
* fix: Interval picker will have to handle location changes since timeSrv wont
* RefreshPicker: Refactoring refresh picker
* RefreshPicker: minor refactoring
This commit is contained in:
committed by
Torkel Ödegaard
parent
d23f50ab23
commit
406ef962fc
@@ -28,6 +28,7 @@
|
||||
"moment": "^2.22.2",
|
||||
"papaparse": "^4.6.3",
|
||||
"react": "^16.8.4",
|
||||
"react-calendar": "^2.18.1",
|
||||
"react-color": "^2.17.0",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.8.4",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PureComponent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export interface Props {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasEventListener: boolean;
|
||||
}
|
||||
|
||||
export class ClickOutsideWrapper extends PureComponent<Props, State> {
|
||||
state = {
|
||||
hasEventListener: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this.onOutsideClick, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.onOutsideClick, false);
|
||||
}
|
||||
|
||||
onOutsideClick = (event: any) => {
|
||||
const domNode = ReactDOM.findDOMNode(this) as Element;
|
||||
|
||||
if (!domNode || !domNode.contains(event.target)) {
|
||||
this.props.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { RefreshPicker } from './RefreshPicker';
|
||||
|
||||
const RefreshSelectStories = storiesOf('UI/RefreshPicker', module);
|
||||
|
||||
RefreshSelectStories.addDecorator(withCenteredStory);
|
||||
|
||||
RefreshSelectStories.add('default', () => {
|
||||
return (
|
||||
<UseState initialState={'1h'}>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<RefreshPicker
|
||||
tooltip="Hello world"
|
||||
value={value}
|
||||
intervals={['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d']}
|
||||
onIntervalChanged={interval => {
|
||||
action('onIntervalChanged fired')(interval);
|
||||
}}
|
||||
onRefresh={() => {
|
||||
action('onRefresh fired')();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SelectOptionItem, ButtonSelect, Tooltip } from '@grafana/ui';
|
||||
|
||||
export const offOption = { label: 'Off', value: '' };
|
||||
export const defaultIntervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];
|
||||
|
||||
export interface Props {
|
||||
intervals?: string[];
|
||||
onRefresh: () => any;
|
||||
onIntervalChanged: (interval: string) => void;
|
||||
value?: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
export class RefreshPicker extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
intervals: defaultIntervals,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
hasNoIntervals = () => {
|
||||
const { intervals } = this.props;
|
||||
// Current implementaion returns an array with length of 1 consisting of
|
||||
// an empty string when auto-refresh is empty in dashboard settings
|
||||
if (!intervals || intervals.length < 1 || (intervals.length === 1 && intervals[0] === '')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
intervalsToOptions = (intervals: string[] = defaultIntervals): SelectOptionItem[] => {
|
||||
const options = intervals.map(interval => ({ label: interval, value: interval }));
|
||||
options.unshift(offOption);
|
||||
return options;
|
||||
};
|
||||
|
||||
onChangeSelect = (item: SelectOptionItem) => {
|
||||
const { onIntervalChanged } = this.props;
|
||||
if (onIntervalChanged) {
|
||||
onIntervalChanged(item.value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onRefresh, intervals, tooltip, value } = this.props;
|
||||
const options = this.intervalsToOptions(this.hasNoIntervals() ? defaultIntervals : intervals);
|
||||
const currentValue = value || '';
|
||||
const selectedValue = options.find(item => item.value === currentValue) || offOption;
|
||||
|
||||
const cssClasses = classNames({
|
||||
'refresh-picker': true,
|
||||
'refresh-picker--refreshing': selectedValue.label !== offOption.label,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cssClasses}>
|
||||
<div className="refresh-picker-buttons">
|
||||
<Tooltip placement="top" content={tooltip}>
|
||||
<button className="btn btn--radius-right-0 navbar-button navbar-button--refresh" onClick={onRefresh}>
|
||||
<i className="fa fa-refresh" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ButtonSelect
|
||||
className="navbar-button--attached btn--radius-left-0"
|
||||
value={selectedValue}
|
||||
label={selectedValue.label}
|
||||
options={options}
|
||||
onChange={this.onChangeSelect}
|
||||
maxMenuHeight={380}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.refresh-picker {
|
||||
position: relative;
|
||||
display: none;
|
||||
|
||||
.refresh-picker-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gf-form-input--form-dropdown {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.gf-form-select-box__menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--refreshing {
|
||||
.select-button-value {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withKnobs, object, text } from '@storybook/addon-knobs';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { SelectOptionItem } from './Select';
|
||||
import { ButtonSelect } from './ButtonSelect';
|
||||
|
||||
const ButtonSelectStories = storiesOf('UI/Select/ButtonSelect', module);
|
||||
|
||||
ButtonSelectStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
|
||||
|
||||
ButtonSelectStories.add('default', () => {
|
||||
const intialState: SelectOptionItem = { label: 'A label', value: 'A value' };
|
||||
const value = object<SelectOptionItem>('Selected Value:', intialState);
|
||||
const options = object<SelectOptionItem[]>('Options:', [
|
||||
intialState,
|
||||
{ label: 'Another label', value: 'Another value' },
|
||||
]);
|
||||
|
||||
return (
|
||||
<UseState initialState={value}>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<ButtonSelect
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={value => {
|
||||
action('onChanged fired')(value);
|
||||
updateValue(value);
|
||||
}}
|
||||
label={value.label ? value.label : ''}
|
||||
className="refresh-select"
|
||||
iconClass={text('iconClass', 'fa fa-clock-o fa-fw')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
88
packages/grafana-ui/src/components/Select/ButtonSelect.tsx
Normal file
88
packages/grafana-ui/src/components/Select/ButtonSelect.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Select, { SelectOptionItem } from './Select';
|
||||
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||
|
||||
interface ButtonComponentProps {
|
||||
label: string | undefined;
|
||||
className: string | undefined;
|
||||
iconClass?: string;
|
||||
}
|
||||
|
||||
const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
|
||||
const { label, className, iconClass } = buttonProps;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={props.innerRef}
|
||||
className={`btn navbar-button navbar-button--tight ${className}`}
|
||||
onClick={props.selectProps.menuIsOpen ? props.selectProps.onMenuClose : props.selectProps.onMenuOpen}
|
||||
onBlur={props.selectProps.onMenuClose}
|
||||
>
|
||||
<div className="select-button">
|
||||
{iconClass && <i className={`select-button-icon ${iconClass}`} />}
|
||||
<span className="select-button-value">{label ? label : ''}</span>
|
||||
<i className="fa fa-caret-down fa-fw" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
className: string | undefined;
|
||||
options: SelectOptionItem[];
|
||||
value: SelectOptionItem;
|
||||
label?: string;
|
||||
iconClass?: string;
|
||||
components?: any;
|
||||
maxMenuHeight?: number;
|
||||
onChange: (item: SelectOptionItem) => void;
|
||||
tooltipContent?: PopperContent<any>;
|
||||
isMenuOpen?: boolean;
|
||||
onOpenMenu?: () => void;
|
||||
onCloseMenu?: () => void;
|
||||
}
|
||||
|
||||
export class ButtonSelect extends PureComponent<Props> {
|
||||
onChange = (item: SelectOptionItem) => {
|
||||
const { onChange } = this.props;
|
||||
onChange(item);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
label,
|
||||
iconClass,
|
||||
components,
|
||||
maxMenuHeight,
|
||||
tooltipContent,
|
||||
isMenuOpen,
|
||||
onOpenMenu,
|
||||
onCloseMenu,
|
||||
} = this.props;
|
||||
const combinedComponents = {
|
||||
...components,
|
||||
Control: ButtonComponent({ label, className, iconClass }),
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
autoFocus
|
||||
backspaceRemovesValue={false}
|
||||
isClearable={false}
|
||||
isSearchable={false}
|
||||
options={options}
|
||||
onChange={this.onChange}
|
||||
defaultValue={value}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
components={combinedComponents}
|
||||
className="gf-form-select-box-button-select"
|
||||
tooltipContent={tooltipContent}
|
||||
isOpen={isMenuOpen}
|
||||
onOpenMenu={onOpenMenu}
|
||||
onCloseMenu={onCloseMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import IndicatorsContainer from './IndicatorsContainer';
|
||||
import NoOptionsMessage from './NoOptionsMessage';
|
||||
import resetSelectStyles from './resetSelectStyles';
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
|
||||
export interface SelectOptionItem {
|
||||
label?: string;
|
||||
@@ -26,7 +28,7 @@ export interface SelectOptionItem {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface CommonProps {
|
||||
export interface CommonProps {
|
||||
defaultValue?: any;
|
||||
getOptionLabel?: (item: SelectOptionItem) => string;
|
||||
getOptionValue?: (item: SelectOptionItem) => string;
|
||||
@@ -42,13 +44,18 @@ interface CommonProps {
|
||||
openMenuOnFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
maxMenuHeight?: number;
|
||||
isLoading: boolean;
|
||||
isLoading?: boolean;
|
||||
noOptionsMessage?: () => string;
|
||||
isMulti?: boolean;
|
||||
backspaceRemovesValue: boolean;
|
||||
backspaceRemovesValue?: boolean;
|
||||
isOpen?: boolean;
|
||||
components?: any;
|
||||
tooltipContent?: PopperContent<any>;
|
||||
onOpenMenu?: () => void;
|
||||
onCloseMenu?: () => void;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
export interface SelectProps {
|
||||
options: SelectOptionItem[];
|
||||
}
|
||||
|
||||
@@ -58,6 +65,26 @@ interface AsyncProps {
|
||||
loadingMessage?: () => string;
|
||||
}
|
||||
|
||||
const wrapInTooltip = (
|
||||
component: React.ReactElement,
|
||||
tooltipContent: PopperContent<any> | undefined,
|
||||
isMenuOpen: boolean | undefined
|
||||
) => {
|
||||
const showTooltip = isMenuOpen ? false : undefined;
|
||||
if (tooltipContent) {
|
||||
return (
|
||||
<Tooltip show={showTooltip} content={tooltipContent} placement="bottom">
|
||||
<div>
|
||||
{/* div needed for tooltip */}
|
||||
{component}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return <div>{component}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
@@ -81,6 +108,28 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
isLoading: false,
|
||||
backspaceRemovesValue: true,
|
||||
maxMenuHeight: 300,
|
||||
menuIsOpen: false,
|
||||
components: {
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
MenuList,
|
||||
Group: SelectOptionGroup,
|
||||
},
|
||||
};
|
||||
|
||||
onOpenMenu = () => {
|
||||
const { onOpenMenu } = this.props;
|
||||
if (onOpenMenu) {
|
||||
onOpenMenu();
|
||||
}
|
||||
};
|
||||
|
||||
onCloseMenu = () => {
|
||||
const { onCloseMenu } = this.props;
|
||||
if (onCloseMenu) {
|
||||
onCloseMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -105,6 +154,9 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
onBlur,
|
||||
maxMenuHeight,
|
||||
noOptionsMessage,
|
||||
isOpen,
|
||||
components,
|
||||
tooltipContent,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
@@ -113,18 +165,12 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
}
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
const selectComponents = { ...Select.defaultProps.components, ...components };
|
||||
return wrapInTooltip(
|
||||
<ReactSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
MenuList,
|
||||
Group: SelectOptionGroup,
|
||||
}}
|
||||
components={selectComponents}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
getOptionLabel={getOptionLabel}
|
||||
@@ -145,7 +191,12 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
menuIsOpen={isOpen}
|
||||
onMenuOpen={this.onOpenMenu}
|
||||
onMenuClose={this.onCloseMenu}
|
||||
/>,
|
||||
tooltipContent,
|
||||
isOpen
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -190,6 +241,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
openMenuOnFocus,
|
||||
maxMenuHeight,
|
||||
isMulti,
|
||||
tooltipContent,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
@@ -199,7 +251,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
return wrapInTooltip(
|
||||
<ReactAsyncSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
@@ -231,7 +283,9 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
/>,
|
||||
tooltipContent,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { components } from '@torkelo/react-select';
|
||||
import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
// https://github.com/JedWatson/react-select/issues/3038
|
||||
interface ExtendedOptionProps extends OptionProps<any> {
|
||||
export interface ExtendedOptionProps extends OptionProps<any> {
|
||||
data: {
|
||||
description?: string;
|
||||
imgUrl?: string;
|
||||
|
||||
@@ -189,3 +189,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box-button-select {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { stringToMs } from '../../utils/string';
|
||||
|
||||
interface Props {
|
||||
func: () => any; // TODO
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export class SetInterval extends PureComponent<Props> {
|
||||
private intervalId = 0;
|
||||
|
||||
componentDidMount() {
|
||||
this.addInterval();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { interval } = this.props;
|
||||
if (interval !== prevProps.interval) {
|
||||
this.clearInterval();
|
||||
this.addInterval();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearInterval();
|
||||
}
|
||||
|
||||
addInterval = () => {
|
||||
const { func, interval } = this.props;
|
||||
|
||||
if (interval) {
|
||||
func().then(() => {
|
||||
if (interval) {
|
||||
this.intervalId = window.setTimeout(() => {
|
||||
this.addInterval();
|
||||
}, stringToMs(interval));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
clearInterval = () => {
|
||||
window.clearTimeout(this.intervalId);
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import React from 'react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { TimePicker } from './TimePicker';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withRighAlignedStory } from '../../utils/storybook/withRightAlignedStory';
|
||||
|
||||
const TimePickerStories = storiesOf('UI/TimePicker', module);
|
||||
export const popoverOptions = {
|
||||
'0': [
|
||||
{
|
||||
from: 'now-2d',
|
||||
to: 'now',
|
||||
display: 'Last 2 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-7d',
|
||||
to: 'now',
|
||||
display: 'Last 7 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
display: 'Last 30 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-90d',
|
||||
to: 'now',
|
||||
display: 'Last 90 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-6M',
|
||||
to: 'now',
|
||||
display: 'Last 6 months',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1y',
|
||||
to: 'now',
|
||||
display: 'Last 1 year',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-2y',
|
||||
to: 'now',
|
||||
display: 'Last 2 years',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-5y',
|
||||
to: 'now',
|
||||
display: 'Last 5 years',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
'1': [
|
||||
{
|
||||
from: 'now-1d/d',
|
||||
to: 'now-1d/d',
|
||||
display: 'Yesterday',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-2d/d',
|
||||
to: 'now-2d/d',
|
||||
display: 'Day before yesterday',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-7d/d',
|
||||
to: 'now-7d/d',
|
||||
display: 'This day last week',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1w/w',
|
||||
to: 'now-1w/w',
|
||||
display: 'Previous week',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1M/M',
|
||||
to: 'now-1M/M',
|
||||
display: 'Previous month',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1y/y',
|
||||
to: 'now-1y/y',
|
||||
display: 'Previous year',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
'2': [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
section: 2,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now',
|
||||
display: 'Today so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now',
|
||||
display: 'This week so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/M',
|
||||
to: 'now/M',
|
||||
display: 'This month',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/M',
|
||||
to: 'now',
|
||||
display: 'This month so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/y',
|
||||
to: 'now/y',
|
||||
display: 'This year',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/y',
|
||||
to: 'now',
|
||||
display: 'This year so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
TimePickerStories.addDecorator(withRighAlignedStory);
|
||||
|
||||
TimePickerStories.add('default', () => {
|
||||
return (
|
||||
<UseState
|
||||
initialState={{
|
||||
from: moment(),
|
||||
to: moment(),
|
||||
raw: { from: 'now-6h' as string | Moment, to: 'now' as string | Moment },
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePicker
|
||||
isTimezoneUtc={false}
|
||||
value={value}
|
||||
tooltipContent="TimePicker tooltip"
|
||||
selectOptions={[
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3, active: false },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3, active: false },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3, active: false },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3, active: false },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3, active: false },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3, active: false },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3, active: false },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3, active: false },
|
||||
]}
|
||||
popoverOptions={popoverOptions}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
}}
|
||||
onMoveBackward={() => {
|
||||
action('onMoveBackward fired')();
|
||||
}}
|
||||
onMoveForward={() => {
|
||||
action('onMoveForward fired')();
|
||||
}}
|
||||
onZoom={() => {
|
||||
action('onZoom fired')();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
300
packages/grafana-ui/src/components/TimePicker/TimePicker.tsx
Normal file
300
packages/grafana-ui/src/components/TimePicker/TimePicker.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { TimeRange, TimeOptions, TimeOption, SelectOptionItem } from '@grafana/ui';
|
||||
import { ButtonSelect } from '@grafana/ui/src/components/Select/ButtonSelect';
|
||||
import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time';
|
||||
import { Props as TimePickerPopoverProps } from './TimePickerPopover';
|
||||
import { TimePickerOptionGroup } from './TimePickerOptionGroup';
|
||||
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
isTimezoneUtc: boolean;
|
||||
popoverOptions: TimeOptions;
|
||||
selectOptions: TimeOption[];
|
||||
timezone?: Timezone;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
onMoveBackward: () => void;
|
||||
onMoveForward: () => void;
|
||||
onZoom: () => void;
|
||||
tooltipContent?: PopperContent<any>;
|
||||
}
|
||||
|
||||
const defaultSelectOptions = [
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3, active: false },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3, active: false },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3, active: false },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3, active: false },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3, active: false },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3, active: false },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3, active: false },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3, active: false },
|
||||
];
|
||||
|
||||
const defaultPopoverOptions = {
|
||||
'0': [
|
||||
{
|
||||
from: 'now-2d',
|
||||
to: 'now',
|
||||
display: 'Last 2 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-7d',
|
||||
to: 'now',
|
||||
display: 'Last 7 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
display: 'Last 30 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-90d',
|
||||
to: 'now',
|
||||
display: 'Last 90 days',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-6M',
|
||||
to: 'now',
|
||||
display: 'Last 6 months',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1y',
|
||||
to: 'now',
|
||||
display: 'Last 1 year',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-2y',
|
||||
to: 'now',
|
||||
display: 'Last 2 years',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-5y',
|
||||
to: 'now',
|
||||
display: 'Last 5 years',
|
||||
section: 0,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
'1': [
|
||||
{
|
||||
from: 'now-1d/d',
|
||||
to: 'now-1d/d',
|
||||
display: 'Yesterday',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-2d/d',
|
||||
to: 'now-2d/d',
|
||||
display: 'Day before yesterday',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-7d/d',
|
||||
to: 'now-7d/d',
|
||||
display: 'This day last week',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1w/w',
|
||||
to: 'now-1w/w',
|
||||
display: 'Previous week',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1M/M',
|
||||
to: 'now-1M/M',
|
||||
display: 'Previous month',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now-1y/y',
|
||||
to: 'now-1y/y',
|
||||
display: 'Previous year',
|
||||
section: 1,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
'2': [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
section: 2,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now',
|
||||
display: 'Today so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now',
|
||||
display: 'This week so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/M',
|
||||
to: 'now/M',
|
||||
display: 'This month',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/M',
|
||||
to: 'now',
|
||||
display: 'This month so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/y',
|
||||
to: 'now/y',
|
||||
display: 'This year',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
from: 'now/y',
|
||||
to: 'now',
|
||||
display: 'This year so far',
|
||||
section: 2,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface State {
|
||||
isMenuOpen: boolean;
|
||||
}
|
||||
|
||||
export class TimePicker extends PureComponent<Props, State> {
|
||||
static defaultSelectOptions = defaultSelectOptions;
|
||||
static defaultPopoverOptions = defaultPopoverOptions;
|
||||
state: State = {
|
||||
isMenuOpen: false,
|
||||
};
|
||||
|
||||
mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => {
|
||||
const { value, popoverOptions, isTimezoneUtc, timezone } = this.props;
|
||||
const options = selectOptions.map(timeOption => {
|
||||
return { label: timeOption.display, value: timeOption };
|
||||
});
|
||||
|
||||
const popoverProps: TimePickerPopoverProps = {
|
||||
value,
|
||||
options: popoverOptions,
|
||||
isTimezoneUtc,
|
||||
timezone,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Custom',
|
||||
expanded: true,
|
||||
options,
|
||||
onPopoverOpen: () => undefined,
|
||||
onPopoverClose: (timeRange: TimeRange) => this.onPopoverClose(timeRange),
|
||||
popoverProps,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
onSelectChanged = (item: SelectOptionItem) => {
|
||||
const { isTimezoneUtc, onChange, timezone } = this.props;
|
||||
onChange(mapTimeOptionToTimeRange(item.value, isTimezoneUtc, timezone));
|
||||
};
|
||||
|
||||
onChangeMenuOpenState = (isOpen: boolean) => {
|
||||
this.setState({
|
||||
isMenuOpen: isOpen,
|
||||
});
|
||||
};
|
||||
onOpenMenu = () => this.onChangeMenuOpenState(true);
|
||||
onCloseMenu = () => this.onChangeMenuOpenState(false);
|
||||
|
||||
onPopoverClose = (timeRange: TimeRange) => {
|
||||
const { onChange } = this.props;
|
||||
onChange(timeRange);
|
||||
// Here we should also close the Select but no sure how to solve this without introducing state in this component
|
||||
// Edit: State introduced
|
||||
this.onCloseMenu();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectOptions: selectTimeOptions,
|
||||
value,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onZoom,
|
||||
tooltipContent,
|
||||
} = this.props;
|
||||
const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions);
|
||||
const rangeString = mapTimeRangeToRangeString(value);
|
||||
const isAbsolute = moment.isMoment(value.raw.to);
|
||||
return (
|
||||
<div className="time-picker">
|
||||
<div className="time-picker-buttons">
|
||||
{isAbsolute && (
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
|
||||
<i className="fa fa-chevron-left" />
|
||||
</button>
|
||||
)}
|
||||
<ButtonSelect
|
||||
className="time-picker-button-select"
|
||||
value={value}
|
||||
label={rangeString}
|
||||
options={options}
|
||||
onChange={this.onSelectChanged}
|
||||
components={{ Group: TimePickerOptionGroup }}
|
||||
iconClass={'fa fa-clock-o fa-fw'}
|
||||
tooltipContent={tooltipContent}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
onOpenMenu={this.onOpenMenu}
|
||||
onCloseMenu={this.onCloseMenu}
|
||||
/>
|
||||
{isAbsolute && (
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={onMoveForward}>
|
||||
<i className="fa fa-chevron-right" />
|
||||
</button>
|
||||
)}
|
||||
<button className="btn navbar-button navbar-button--zoom" onClick={onZoom}>
|
||||
<i className="fa fa-search-minus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
|
||||
const TimePickerCalendarStories = storiesOf('UI/TimePicker/TimePickerCalendar', module);
|
||||
|
||||
TimePickerCalendarStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimePickerCalendarStories.add('default', () => (
|
||||
<UseState initialState={'now-6h' as string | Moment}>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePickerCalendar
|
||||
isTimezoneUtc={false}
|
||||
value={value}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
));
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { TimeFragment } from '@grafana/ui';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
|
||||
import { stringToMoment } from './time';
|
||||
|
||||
export interface Props {
|
||||
value: TimeFragment;
|
||||
isTimezoneUtc: boolean;
|
||||
roundup?: boolean;
|
||||
timezone?: Timezone;
|
||||
onChange: (value: Moment) => void;
|
||||
}
|
||||
|
||||
export class TimePickerCalendar extends PureComponent<Props> {
|
||||
onCalendarChange = (date: Date | Date[]) => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
if (Array.isArray(date)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(moment(date));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, isTimezoneUtc, roundup, timezone } = this.props;
|
||||
const dateValue = moment.isMoment(value)
|
||||
? value.toDate()
|
||||
: stringToMoment(value, isTimezoneUtc, roundup, timezone).toDate();
|
||||
const calendarValue = dateValue instanceof Date && !isNaN(dateValue.getTime()) ? dateValue : moment().toDate();
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
value={calendarValue}
|
||||
next2Label={null}
|
||||
prev2Label={null}
|
||||
className="time-picker-calendar"
|
||||
tileClassName="time-picker-calendar-tile"
|
||||
onChange={this.onCalendarChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { TimeFragment, TIME_FORMAT, Input } from '@grafana/ui';
|
||||
|
||||
import { stringToMoment, isValidTimeString } from './time';
|
||||
|
||||
export interface Props {
|
||||
value: TimeFragment;
|
||||
isTimezoneUtc: boolean;
|
||||
roundup?: boolean;
|
||||
timezone?: string;
|
||||
onChange: (value: string, isValid: boolean) => void;
|
||||
}
|
||||
|
||||
export class TimePickerInput extends PureComponent<Props> {
|
||||
isValid = (value: string) => {
|
||||
const { isTimezoneUtc } = this.props;
|
||||
|
||||
if (value.indexOf('now') !== -1) {
|
||||
const isValid = isValidTimeString(value);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
const parsed = stringToMoment(value, isTimezoneUtc);
|
||||
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 (moment.isMoment(value)) {
|
||||
return value.format(TIME_FORMAT);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = 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' : ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import moment from 'moment';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { TimePickerOptionGroup } from './TimePickerOptionGroup';
|
||||
import { TimeRange } from '../../types/time';
|
||||
import { withRighAlignedStory } from '../../utils/storybook/withRightAlignedStory';
|
||||
import { popoverOptions } from './TimePicker.story';
|
||||
|
||||
const TimePickerOptionGroupStories = storiesOf('UI/TimePicker/TimePickerOptionGroup', module);
|
||||
|
||||
TimePickerOptionGroupStories.addDecorator(withRighAlignedStory);
|
||||
|
||||
const data = {
|
||||
isPopoverOpen: false,
|
||||
onPopoverOpen: () => {
|
||||
action('onPopoverOpen fired')();
|
||||
},
|
||||
onPopoverClose: (timeRange: TimeRange) => {
|
||||
action('onPopoverClose fired')(timeRange);
|
||||
},
|
||||
popoverProps: {
|
||||
value: { from: moment(), to: moment(), raw: { from: 'now/d', to: 'now/d' } },
|
||||
options: popoverOptions,
|
||||
isTimezoneUtc: false,
|
||||
onChange: (timeRange: TimeRange) => {
|
||||
action('onChange fired')(timeRange);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
TimePickerOptionGroupStories.add('default', () => (
|
||||
<TimePickerOptionGroup
|
||||
clearValue={() => {}}
|
||||
className={''}
|
||||
cx={() => {}}
|
||||
getStyles={(name, props) => ({})}
|
||||
getValue={() => {}}
|
||||
hasValue
|
||||
isMulti={false}
|
||||
options={[]}
|
||||
selectOption={() => {}}
|
||||
selectProps={''}
|
||||
setValue={(value, action) => {}}
|
||||
label={'Custom'}
|
||||
children={null}
|
||||
Heading={(null as any) as ComponentType<any>}
|
||||
data={data}
|
||||
/>
|
||||
));
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
import { GroupProps } from 'react-select/lib/components/Group';
|
||||
import { Popper } from '@grafana/ui/src/components/Tooltip/Popper';
|
||||
import { Props as TimePickerProps, TimePickerPopover } from './TimePickerPopover';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
export interface DataProps {
|
||||
onPopoverOpen: () => void;
|
||||
onPopoverClose: (timeRange: TimeRange) => void;
|
||||
popoverProps: TimePickerProps;
|
||||
}
|
||||
|
||||
interface Props extends GroupProps<any> {
|
||||
data: DataProps;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
export class TimePickerOptionGroup extends PureComponent<Props, State> {
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
state: State = { isPopoverOpen: false };
|
||||
|
||||
onClick = () => {
|
||||
this.setState({ isPopoverOpen: true });
|
||||
this.props.data.onPopoverOpen();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, label } = this.props;
|
||||
const { isPopoverOpen } = this.state;
|
||||
const { onPopoverClose } = this.props.data;
|
||||
const popover = TimePickerPopover;
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...this.props.data.popoverProps,
|
||||
onChange: (timeRange: TimeRange) => {
|
||||
onPopoverClose(timeRange);
|
||||
this.setState({ isPopoverOpen: false });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-select-box__option-group">
|
||||
<div className="gf-form-select-box__option-group__header" ref={this.pickerTriggerRef} onClick={this.onClick}>
|
||||
<span className="flex-grow-1">{label}</span>
|
||||
<i className="fa fa-calendar fa-fw" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
<div>
|
||||
{this.pickerTriggerRef.current && (
|
||||
<Popper
|
||||
show={isPopoverOpen}
|
||||
content={popoverElement}
|
||||
referenceElement={this.pickerTriggerRef.current}
|
||||
placement={'left-start'}
|
||||
wrapperClassName="time-picker-popover-popper"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import moment, { Moment } from 'moment';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TimePickerPopover } from './TimePickerPopover';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { popoverOptions } from './TimePicker.story';
|
||||
|
||||
const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module);
|
||||
|
||||
TimePickerPopoverStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimePickerPopoverStories.add('default', () => (
|
||||
<UseState
|
||||
initialState={{
|
||||
from: moment(),
|
||||
to: moment(),
|
||||
raw: { from: 'now-6h' as string | Moment, to: 'now' as string | Moment },
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePickerPopover
|
||||
value={value}
|
||||
isTimezoneUtc={false}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
updateValue(timeRange);
|
||||
}}
|
||||
options={popoverOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
));
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { Component, SyntheticEvent } from 'react';
|
||||
import { TimeRange, TimeOptions, TimeOption } from '@grafana/ui';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import { TimePickerInput } from './TimePickerInput';
|
||||
import { mapTimeOptionToTimeRange } from './time';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
options: TimeOptions;
|
||||
isTimezoneUtc: boolean;
|
||||
timezone?: Timezone;
|
||||
onChange?: (timeRange: TimeRange) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
value: TimeRange;
|
||||
isFromInputValid: boolean;
|
||||
isToInputValid: boolean;
|
||||
}
|
||||
|
||||
export class TimePickerPopover extends Component<Props, State> {
|
||||
static popoverClassName = 'time-picker-popover';
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { value: props.value, isFromInputValid: true, isToInputValid: true };
|
||||
}
|
||||
|
||||
onFromInputChanged = (value: string, valid: boolean) => {
|
||||
this.setState({
|
||||
value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
|
||||
isFromInputValid: valid,
|
||||
});
|
||||
};
|
||||
|
||||
onToInputChanged = (value: string, valid: boolean) => {
|
||||
this.setState({
|
||||
value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } },
|
||||
isToInputValid: valid,
|
||||
});
|
||||
};
|
||||
|
||||
onFromCalendarChanged = (value: Moment) => {
|
||||
this.setState({
|
||||
value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
|
||||
});
|
||||
};
|
||||
|
||||
onToCalendarChanged = (value: Moment) => {
|
||||
this.setState({
|
||||
value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } },
|
||||
});
|
||||
};
|
||||
|
||||
onTimeOptionClick = (timeOption: TimeOption) => {
|
||||
const { isTimezoneUtc, timezone, onChange } = this.props;
|
||||
|
||||
if (onChange) {
|
||||
onChange(mapTimeOptionToTimeRange(timeOption, isTimezoneUtc, timezone));
|
||||
}
|
||||
};
|
||||
|
||||
onApplyClick = () => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(this.state.value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, isTimezoneUtc, timezone } = this.props;
|
||||
const { isFromInputValid, isToInputValid, value } = this.state;
|
||||
const isValid = isFromInputValid && isToInputValid;
|
||||
|
||||
return (
|
||||
<div className={TimePickerPopover.popoverClassName}>
|
||||
<div className="time-picker-popover-box">
|
||||
<div className="time-picker-popover-box-header">
|
||||
<span className="time-picker-popover-box-title">Quick ranges</span>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-body">
|
||||
{Object.keys(options).map(key => {
|
||||
return (
|
||||
<ul key={`popover-quickranges-${key}`}>
|
||||
{options[key].map(timeOption => (
|
||||
<li
|
||||
key={`popover-timeoption-${timeOption.from}-${timeOption.to}`}
|
||||
className={timeOption.active ? 'active' : ''}
|
||||
>
|
||||
<a
|
||||
onClick={(event: SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
this.onTimeOptionClick(timeOption);
|
||||
}}
|
||||
>
|
||||
{timeOption.display}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-box">
|
||||
<div className="time-picker-popover-box-header">
|
||||
<span className="time-picker-popover-box-title">Custom range</span>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-body">
|
||||
<div className="time-picker-popover-box-body-custom-ranges">
|
||||
<div className="time-picker-popover-box-body-custom-ranges-input">
|
||||
<span>From:</span>
|
||||
<TimePickerInput
|
||||
isTimezoneUtc={isTimezoneUtc}
|
||||
roundup={false}
|
||||
timezone={timezone}
|
||||
value={value.raw.from}
|
||||
onChange={this.onFromInputChanged}
|
||||
/>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar
|
||||
isTimezoneUtc={isTimezoneUtc}
|
||||
roundup={false}
|
||||
timezone={timezone}
|
||||
value={value.raw.from}
|
||||
onChange={this.onFromCalendarChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-body-custom-ranges">
|
||||
<div className="time-picker-popover-box-body-custom-ranges-input">
|
||||
<span>To:</span>
|
||||
<TimePickerInput
|
||||
isTimezoneUtc={isTimezoneUtc}
|
||||
roundup={true}
|
||||
timezone={timezone}
|
||||
value={value.raw.to}
|
||||
onChange={this.onToInputChanged}
|
||||
/>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar
|
||||
isTimezoneUtc={isTimezoneUtc}
|
||||
roundup={true}
|
||||
timezone={timezone}
|
||||
value={value.raw.to}
|
||||
onChange={this.onToCalendarChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-box-footer">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn gf-form-btn btn-success"
|
||||
disabled={!isValid}
|
||||
onClick={this.onApplyClick}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
189
packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
Normal file
189
packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
Normal file
@@ -0,0 +1,189 @@
|
||||
.time-picker {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.time-picker-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.time-picker-popover-popper {
|
||||
z-index: $zindex-timepicker-popover;
|
||||
}
|
||||
|
||||
.time-picker-popover {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
border: 1px solid $popover-border-color;
|
||||
border-radius: $border-radius;
|
||||
background-color: $popover-border-color;
|
||||
color: $popover-color;
|
||||
|
||||
.time-picker-popover-box {
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
|
||||
ul {
|
||||
padding-right: $spacer;
|
||||
padding-top: $spacer;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
line-height: 22px;
|
||||
display: list-item;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
li.active {
|
||||
border-bottom: 1px solid $blue;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-box-body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-box-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.time-picker-popover-box:first-child {
|
||||
border-right: 1px ridge;
|
||||
}
|
||||
|
||||
.time-picker-popover-box-body-custom-ranges:first-child {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
|
||||
.time-picker-popover-box-body-custom-ranges-input {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin: $spacer 0;
|
||||
|
||||
.our-custom-wrapper-class {
|
||||
margin-left: $spacer;
|
||||
width: 100%;
|
||||
|
||||
.time-picker-input-error {
|
||||
box-shadow: inset 0 0px 5px $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-box-footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
margin-top: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-calendar {
|
||||
border: 1px solid $popover-border-color;
|
||||
max-width: 220px;
|
||||
color: $black;
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
background-color: $popover-border-color;
|
||||
text-align: center;
|
||||
|
||||
abbr {
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
color: $popover-color;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-calendar-tile {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
border: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
button.time-picker-calendar-tile:hover {
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation > button:focus,
|
||||
.time-picker-calendar-tile:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
color: $blue;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1116px) {
|
||||
.time-picker-popover {
|
||||
margin-left: $spacer;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.time-picker-popover-box {
|
||||
padding: $spacer / 2 $spacer;
|
||||
|
||||
.time-picker-popover-box-title {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-box:first-child {
|
||||
border-right: none;
|
||||
border-bottom: 1px ridge;
|
||||
}
|
||||
|
||||
.time-picker-popover-box:last-child {
|
||||
.time-picker-popover-box-body {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.time-picker-popover-box-body-custom-ranges:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-popover-box-footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
margin-top: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.time-picker-calendar {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 746px) {
|
||||
.time-picker-popover {
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
||||
44
packages/grafana-ui/src/components/TimePicker/time.ts
Normal file
44
packages/grafana-ui/src/components/TimePicker/time.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import moment, { Moment } from 'moment';
|
||||
import { TimeOption, TimeRange, TIME_FORMAT } from '@grafana/ui';
|
||||
|
||||
import * as dateMath from '../../../../../public/app/core/utils/datemath';
|
||||
import { describeTimeRange } from '../../../../../public/app/core/utils/rangeutil';
|
||||
|
||||
export const mapTimeOptionToTimeRange = (
|
||||
timeOption: TimeOption,
|
||||
isTimezoneUtc: boolean,
|
||||
timezone?: dateMath.Timezone
|
||||
): TimeRange => {
|
||||
const fromMoment = stringToMoment(timeOption.from, isTimezoneUtc, false, timezone);
|
||||
const toMoment = stringToMoment(timeOption.to, isTimezoneUtc, true, timezone);
|
||||
|
||||
return { from: fromMoment, to: toMoment, raw: { from: timeOption.from, to: timeOption.to } };
|
||||
};
|
||||
|
||||
export const stringToMoment = (
|
||||
value: string,
|
||||
isTimezoneUtc: boolean,
|
||||
roundUp?: boolean,
|
||||
timezone?: dateMath.Timezone
|
||||
): Moment => {
|
||||
if (value.indexOf('now') !== -1) {
|
||||
if (!dateMath.isValid(value)) {
|
||||
return moment();
|
||||
}
|
||||
|
||||
const parsed = dateMath.parse(value, roundUp, timezone);
|
||||
return parsed || moment();
|
||||
}
|
||||
|
||||
if (isTimezoneUtc) {
|
||||
return moment.utc(value, TIME_FORMAT);
|
||||
}
|
||||
|
||||
return moment(value, TIME_FORMAT);
|
||||
};
|
||||
|
||||
export const mapTimeRangeToRangeString = (timeRange: TimeRange): string => {
|
||||
return describeTimeRange(timeRange.raw);
|
||||
};
|
||||
|
||||
export const isValidTimeString = (text: string) => dateMath.isValid(text);
|
||||
@@ -13,11 +13,18 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
|
||||
return (
|
||||
<PopperController {...controllerProps}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
{
|
||||
/* Override internal 'show' state if passed in as prop */
|
||||
}
|
||||
const payloadProps = {
|
||||
...popperProps,
|
||||
show: controllerProps.show !== undefined ? controllerProps.show : popperProps.show,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{tooltipTriggerRef.current && (
|
||||
<Popper
|
||||
{...popperProps}
|
||||
{...payloadProps}
|
||||
onMouseEnter={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
referenceElement={tooltipTriggerRef.current}
|
||||
|
||||
@@ -12,3 +12,5 @@
|
||||
@import 'EmptySearchResult/EmptySearchResult';
|
||||
@import 'FormField/FormField';
|
||||
@import 'BarGauge/BarGauge';
|
||||
@import 'RefreshPicker/RefreshPicker';
|
||||
@import 'TimePicker/TimePicker';
|
||||
|
||||
@@ -12,6 +12,7 @@ export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
export { ButtonSelect } from './Select/ButtonSelect';
|
||||
|
||||
// Forms
|
||||
export { FormLabel } from './FormLabel/FormLabel';
|
||||
@@ -31,6 +32,10 @@ export { PieChart, PieChartType } from './PieChart/PieChart';
|
||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||
export { StatsPicker } from './StatsPicker/StatsPicker';
|
||||
export { Input, InputStatus } from './Input/Input';
|
||||
export { RefreshPicker } from './RefreshPicker/RefreshPicker';
|
||||
|
||||
// Renderless
|
||||
export { SetInterval } from './SetInterval/SetInterval';
|
||||
|
||||
export { Table } from './Table/Table';
|
||||
export { TableInputCSV } from './Table/TableInputCSV';
|
||||
@@ -41,6 +46,6 @@ export { Gauge } from './Gauge/Gauge';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { BarGauge } from './BarGauge/BarGauge';
|
||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
export * from './SingleStatShared/shared';
|
||||
|
||||
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||
|
||||
@@ -171,6 +171,7 @@ $zindex-tooltip: ${theme.zIndex.tooltip};
|
||||
$zindex-modal-backdrop: ${theme.zIndex.modalBackdrop};
|
||||
$zindex-modal: ${theme.zIndex.modal};
|
||||
$zindex-typeahead: ${theme.zIndex.typeahead};
|
||||
$zindex-timepicker-popover: 1070;
|
||||
|
||||
// Buttons
|
||||
//
|
||||
|
||||
@@ -15,3 +15,19 @@ export interface IntervalValues {
|
||||
interval: string; // 10s,5m
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
export interface TimeOption {
|
||||
from: string;
|
||||
to: string;
|
||||
display: string;
|
||||
section: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface TimeOptions {
|
||||
[key: string]: TimeOption[];
|
||||
}
|
||||
|
||||
export type TimeFragment = string | Moment;
|
||||
|
||||
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './logs';
|
||||
export * from './labels';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
export * from './validate';
|
||||
export * from './object';
|
||||
|
||||
8
packages/grafana-ui/src/utils/object.ts
Normal file
8
packages/grafana-ui/src/utils/object.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const objRemoveUndefined = (obj: any) => {
|
||||
return Object.keys(obj).reduce((acc: any, key) => {
|
||||
if (obj[key] !== undefined) {
|
||||
acc[key] = obj[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { RenderFunction } from '@storybook/react';
|
||||
|
||||
const RightAlignedStory: React.FunctionComponent<{}> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh ',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-end',
|
||||
marginRight: '20px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const withRighAlignedStory = (story: RenderFunction) => <RightAlignedStory>{story()}</RightAlignedStory>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringToJsRegex } from '@grafana/ui';
|
||||
import { stringToJsRegex, stringToMs } from '@grafana/ui';
|
||||
|
||||
describe('stringToJsRegex', () => {
|
||||
it('should parse the valid regex value', () => {
|
||||
@@ -13,3 +13,41 @@ describe('stringToJsRegex', () => {
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToMs', () => {
|
||||
it('should return zero if no input', () => {
|
||||
const output = stringToMs('');
|
||||
expect(output).toBe(0);
|
||||
});
|
||||
|
||||
it('should return its input, as int, if no unit is supplied', () => {
|
||||
const output = stringToMs('1000');
|
||||
expect(output).toBe(1000);
|
||||
});
|
||||
|
||||
it('should convert 3s to 3000', () => {
|
||||
const output = stringToMs('3s');
|
||||
expect(output).toBe(3000);
|
||||
});
|
||||
|
||||
it('should convert 2m to 120000', () => {
|
||||
const output = stringToMs('2m');
|
||||
expect(output).toBe(120000);
|
||||
});
|
||||
|
||||
it('should convert 2h to 7200000', () => {
|
||||
const output = stringToMs('2h');
|
||||
expect(output).toBe(7200000);
|
||||
});
|
||||
|
||||
it('should convert 2d to 172800000', () => {
|
||||
const output = stringToMs('2d');
|
||||
expect(output).toBe(172800000);
|
||||
});
|
||||
|
||||
it('should throw on unsupported unit', () => {
|
||||
expect(() => {
|
||||
stringToMs('1y');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SelectOptionItem } from './../components/Select/Select';
|
||||
|
||||
export function stringToJsRegex(str: string): RegExp {
|
||||
if (str[0] !== '/') {
|
||||
return new RegExp('^' + str + '$');
|
||||
@@ -11,3 +13,39 @@ export function stringToJsRegex(str: string): RegExp {
|
||||
|
||||
return new RegExp(match[1], match[2]);
|
||||
}
|
||||
|
||||
export function stringToMs(str: string): number {
|
||||
if (!str) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nr = parseInt(str, 10);
|
||||
const unit = str.substr(String(nr).length);
|
||||
const s = 1000;
|
||||
const m = s * 60;
|
||||
const h = m * 60;
|
||||
const d = h * 24;
|
||||
|
||||
switch (unit) {
|
||||
case 's':
|
||||
return nr * s;
|
||||
case 'm':
|
||||
return nr * m;
|
||||
case 'h':
|
||||
return nr * h;
|
||||
case 'd':
|
||||
return nr * d;
|
||||
default:
|
||||
if (!unit) {
|
||||
return isNaN(nr) ? 0 : nr;
|
||||
}
|
||||
throw new Error('Not supported unit: ' + unit);
|
||||
}
|
||||
}
|
||||
|
||||
export function getIntervalFromString(strInterval: string): SelectOptionItem {
|
||||
return {
|
||||
label: strInterval,
|
||||
value: stringToMs(strInterval),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user