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",
|
"moment": "^2.22.2",
|
||||||
"papaparse": "^4.6.3",
|
"papaparse": "^4.6.3",
|
||||||
"react": "^16.8.4",
|
"react": "^16.8.4",
|
||||||
|
"react-calendar": "^2.18.1",
|
||||||
"react-color": "^2.17.0",
|
"react-color": "^2.17.0",
|
||||||
"react-custom-scrollbars": "^4.2.1",
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
"react-dom": "^16.8.4",
|
"react-dom": "^16.8.4",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class ClickOutsideWrapper extends PureComponent<Props, State> {
|
|||||||
window.removeEventListener('click', this.onOutsideClick, false);
|
window.removeEventListener('click', this.onOutsideClick, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOutsideClick = event => {
|
onOutsideClick = (event: any) => {
|
||||||
const domNode = ReactDOM.findDOMNode(this) as Element;
|
const domNode = ReactDOM.findDOMNode(this) as Element;
|
||||||
|
|
||||||
if (!domNode || !domNode.contains(event.target)) {
|
if (!domNode || !domNode.contains(event.target)) {
|
||||||
@@ -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 NoOptionsMessage from './NoOptionsMessage';
|
||||||
import resetSelectStyles from './resetSelectStyles';
|
import resetSelectStyles from './resetSelectStyles';
|
||||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||||
|
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||||
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
export interface SelectOptionItem {
|
export interface SelectOptionItem {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -26,7 +28,7 @@ export interface SelectOptionItem {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommonProps {
|
export interface CommonProps {
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
getOptionLabel?: (item: SelectOptionItem) => string;
|
getOptionLabel?: (item: SelectOptionItem) => string;
|
||||||
getOptionValue?: (item: SelectOptionItem) => string;
|
getOptionValue?: (item: SelectOptionItem) => string;
|
||||||
@@ -42,13 +44,18 @@ interface CommonProps {
|
|||||||
openMenuOnFocus?: boolean;
|
openMenuOnFocus?: boolean;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
maxMenuHeight?: number;
|
maxMenuHeight?: number;
|
||||||
isLoading: boolean;
|
isLoading?: boolean;
|
||||||
noOptionsMessage?: () => string;
|
noOptionsMessage?: () => string;
|
||||||
isMulti?: boolean;
|
isMulti?: boolean;
|
||||||
backspaceRemovesValue: boolean;
|
backspaceRemovesValue?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
components?: any;
|
||||||
|
tooltipContent?: PopperContent<any>;
|
||||||
|
onOpenMenu?: () => void;
|
||||||
|
onCloseMenu?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps {
|
export interface SelectProps {
|
||||||
options: SelectOptionItem[];
|
options: SelectOptionItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +65,26 @@ interface AsyncProps {
|
|||||||
loadingMessage?: () => string;
|
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) => {
|
export const MenuList = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<components.MenuList {...props}>
|
<components.MenuList {...props}>
|
||||||
@@ -81,6 +108,28 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
backspaceRemovesValue: true,
|
backspaceRemovesValue: true,
|
||||||
maxMenuHeight: 300,
|
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() {
|
render() {
|
||||||
@@ -105,6 +154,9 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
onBlur,
|
onBlur,
|
||||||
maxMenuHeight,
|
maxMenuHeight,
|
||||||
noOptionsMessage,
|
noOptionsMessage,
|
||||||
|
isOpen,
|
||||||
|
components,
|
||||||
|
tooltipContent,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let widthClass = '';
|
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);
|
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||||
|
const selectComponents = { ...Select.defaultProps.components, ...components };
|
||||||
return (
|
return wrapInTooltip(
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
components={{
|
components={selectComponents}
|
||||||
Option: SelectOption,
|
|
||||||
SingleValue,
|
|
||||||
IndicatorsContainer,
|
|
||||||
MenuList,
|
|
||||||
Group: SelectOptionGroup,
|
|
||||||
}}
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
getOptionLabel={getOptionLabel}
|
getOptionLabel={getOptionLabel}
|
||||||
@@ -145,7 +191,12 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={noOptionsMessage}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
backspaceRemovesValue={backspaceRemovesValue}
|
backspaceRemovesValue={backspaceRemovesValue}
|
||||||
/>
|
menuIsOpen={isOpen}
|
||||||
|
onMenuOpen={this.onOpenMenu}
|
||||||
|
onMenuClose={this.onCloseMenu}
|
||||||
|
/>,
|
||||||
|
tooltipContent,
|
||||||
|
isOpen
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +241,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
|||||||
openMenuOnFocus,
|
openMenuOnFocus,
|
||||||
maxMenuHeight,
|
maxMenuHeight,
|
||||||
isMulti,
|
isMulti,
|
||||||
|
tooltipContent,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let widthClass = '';
|
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);
|
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||||
|
|
||||||
return (
|
return wrapInTooltip(
|
||||||
<ReactAsyncSelect
|
<ReactAsyncSelect
|
||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
@@ -231,7 +283,9 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
|||||||
maxMenuHeight={maxMenuHeight}
|
maxMenuHeight={maxMenuHeight}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
backspaceRemovesValue={backspaceRemovesValue}
|
backspaceRemovesValue={backspaceRemovesValue}
|
||||||
/>
|
/>,
|
||||||
|
tooltipContent,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { components } from '@torkelo/react-select';
|
|||||||
import { OptionProps } from 'react-select/lib/components/Option';
|
import { OptionProps } from 'react-select/lib/components/Option';
|
||||||
|
|
||||||
// https://github.com/JedWatson/react-select/issues/3038
|
// https://github.com/JedWatson/react-select/issues/3038
|
||||||
interface ExtendedOptionProps extends OptionProps<any> {
|
export interface ExtendedOptionProps extends OptionProps<any> {
|
||||||
data: {
|
data: {
|
||||||
description?: string;
|
description?: string;
|
||||||
imgUrl?: string;
|
imgUrl?: string;
|
||||||
|
|||||||
@@ -189,3 +189,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
padding-right: 2px;
|
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 (
|
return (
|
||||||
<PopperController {...controllerProps}>
|
<PopperController {...controllerProps}>
|
||||||
{(showPopper, hidePopper, popperProps) => {
|
{(showPopper, hidePopper, popperProps) => {
|
||||||
|
{
|
||||||
|
/* Override internal 'show' state if passed in as prop */
|
||||||
|
}
|
||||||
|
const payloadProps = {
|
||||||
|
...popperProps,
|
||||||
|
show: controllerProps.show !== undefined ? controllerProps.show : popperProps.show,
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tooltipTriggerRef.current && (
|
{tooltipTriggerRef.current && (
|
||||||
<Popper
|
<Popper
|
||||||
{...popperProps}
|
{...payloadProps}
|
||||||
onMouseEnter={showPopper}
|
onMouseEnter={showPopper}
|
||||||
onMouseLeave={hidePopper}
|
onMouseLeave={hidePopper}
|
||||||
referenceElement={tooltipTriggerRef.current}
|
referenceElement={tooltipTriggerRef.current}
|
||||||
|
|||||||
@@ -12,3 +12,5 @@
|
|||||||
@import 'EmptySearchResult/EmptySearchResult';
|
@import 'EmptySearchResult/EmptySearchResult';
|
||||||
@import 'FormField/FormField';
|
@import 'FormField/FormField';
|
||||||
@import 'BarGauge/BarGauge';
|
@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 { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||||
|
export { ButtonSelect } from './Select/ButtonSelect';
|
||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
export { FormLabel } from './FormLabel/FormLabel';
|
export { FormLabel } from './FormLabel/FormLabel';
|
||||||
@@ -31,6 +32,10 @@ export { PieChart, PieChartType } from './PieChart/PieChart';
|
|||||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||||
export { StatsPicker } from './StatsPicker/StatsPicker';
|
export { StatsPicker } from './StatsPicker/StatsPicker';
|
||||||
export { Input, InputStatus } from './Input/Input';
|
export { Input, InputStatus } from './Input/Input';
|
||||||
|
export { RefreshPicker } from './RefreshPicker/RefreshPicker';
|
||||||
|
|
||||||
|
// Renderless
|
||||||
|
export { SetInterval } from './SetInterval/SetInterval';
|
||||||
|
|
||||||
export { Table } from './Table/Table';
|
export { Table } from './Table/Table';
|
||||||
export { TableInputCSV } from './Table/TableInputCSV';
|
export { TableInputCSV } from './Table/TableInputCSV';
|
||||||
@@ -41,6 +46,6 @@ export { Gauge } from './Gauge/Gauge';
|
|||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { BarGauge } from './BarGauge/BarGauge';
|
export { BarGauge } from './BarGauge/BarGauge';
|
||||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||||
|
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
|
||||||
export * from './SingleStatShared/shared';
|
export * from './SingleStatShared/shared';
|
||||||
|
|
||||||
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ $zindex-tooltip: ${theme.zIndex.tooltip};
|
|||||||
$zindex-modal-backdrop: ${theme.zIndex.modalBackdrop};
|
$zindex-modal-backdrop: ${theme.zIndex.modalBackdrop};
|
||||||
$zindex-modal: ${theme.zIndex.modal};
|
$zindex-modal: ${theme.zIndex.modal};
|
||||||
$zindex-typeahead: ${theme.zIndex.typeahead};
|
$zindex-typeahead: ${theme.zIndex.typeahead};
|
||||||
|
$zindex-timepicker-popover: 1070;
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -15,3 +15,19 @@ export interface IntervalValues {
|
|||||||
interval: string; // 10s,5m
|
interval: string; // 10s,5m
|
||||||
intervalMs: number;
|
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 * from './labels';
|
||||||
export { getMappedValue } from './valueMappings';
|
export { getMappedValue } from './valueMappings';
|
||||||
export * from './validate';
|
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', () => {
|
describe('stringToJsRegex', () => {
|
||||||
it('should parse the valid regex value', () => {
|
it('should parse the valid regex value', () => {
|
||||||
@@ -13,3 +13,41 @@ describe('stringToJsRegex', () => {
|
|||||||
}).toThrow();
|
}).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 {
|
export function stringToJsRegex(str: string): RegExp {
|
||||||
if (str[0] !== '/') {
|
if (str[0] !== '/') {
|
||||||
return new RegExp('^' + str + '$');
|
return new RegExp('^' + str + '$');
|
||||||
@@ -11,3 +13,39 @@ export function stringToJsRegex(str: string): RegExp {
|
|||||||
|
|
||||||
return new RegExp(match[1], match[2]);
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,86 +4,88 @@ exports[`TeamPicker renders correctly 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="user-picker"
|
className="user-picker"
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
|
||||||
Select a team
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__input"
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
style={
|
>
|
||||||
Object {
|
Select a team
|
||||||
"display": "inline-block",
|
</div>
|
||||||
}
|
<div
|
||||||
}
|
className="css-0"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-2-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,86 +4,88 @@ exports[`UserPicker renders correctly 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="user-picker"
|
className="user-picker"
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
|
||||||
Select user
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__input"
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
style={
|
>
|
||||||
Object {
|
Select user
|
||||||
"display": "inline-block",
|
</div>
|
||||||
}
|
<div
|
||||||
}
|
className="css-0"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-2-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
const units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||||
|
|
||||||
type Timezone = 'utc';
|
export type Timezone = 'utc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses different types input to a moment instance. There is a specific formatting language that can be used
|
* Parses different types input to a moment instance. There is a specific formatting language that can be used
|
||||||
@@ -88,7 +89,8 @@ export function isValid(text: string | moment.Moment): boolean {
|
|||||||
* @param time
|
* @param time
|
||||||
* @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
|
* @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
|
||||||
*/
|
*/
|
||||||
export function parseDateMath(mathString: string, time: moment.Moment, roundUp?: boolean): moment.Moment | undefined {
|
// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any`
|
||||||
|
export function parseDateMath(mathString: string, time: any, roundUp?: boolean): moment.Moment | undefined {
|
||||||
const dateTime = time;
|
const dateTime = time;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const len = mathString.length;
|
const len = mathString.length;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ describe('state functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns a valid Explore state from a compact URL parameter', () => {
|
it('returns a valid Explore state from a compact URL parameter', () => {
|
||||||
const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D';
|
const paramValue = '%5B"now-1h","now","Local","5m",%7B"expr":"metric"%7D,"ui"%5D';
|
||||||
expect(parseUrlState(paramValue)).toMatchObject({
|
expect(parseUrlState(paramValue)).toMatchObject({
|
||||||
datasource: 'Local',
|
datasource: 'Local',
|
||||||
queries: [{ expr: 'metric' }],
|
queries: [{ expr: 'metric' }],
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function getExploreUrl(
|
|||||||
) {
|
) {
|
||||||
let exploreDatasource = panelDatasource;
|
let exploreDatasource = panelDatasource;
|
||||||
let exploreTargets: DataQuery[] = panelTargets;
|
let exploreTargets: DataQuery[] = panelTargets;
|
||||||
let url;
|
let url: string;
|
||||||
|
|
||||||
// Mixed datasources need to choose only one datasource
|
// Mixed datasources need to choose only one datasource
|
||||||
if (panelDatasource.meta.id === 'mixed' && panelTargets) {
|
if (panelDatasource.meta.id === 'mixed' && panelTargets) {
|
||||||
@@ -191,7 +191,12 @@ export const safeParseJson = (text: string) => {
|
|||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||||
const parsed = safeParseJson(initial);
|
const parsed = safeParseJson(initial);
|
||||||
const errorResult = { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE };
|
const errorResult = {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
ui: DEFAULT_UI_STATE,
|
||||||
|
};
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return errorResult;
|
return errorResult;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@@ -5,7 +6,7 @@ import { RawTimeRange } from '@grafana/ui';
|
|||||||
|
|
||||||
import * as dateMath from './datemath';
|
import * as dateMath from './datemath';
|
||||||
|
|
||||||
const spans = {
|
const spans: { [key: string]: { display: string; section?: number } } = {
|
||||||
s: { display: 'second' },
|
s: { display: 'second' },
|
||||||
m: { display: 'minute' },
|
m: { display: 'minute' },
|
||||||
h: { display: 'hour' },
|
h: { display: 'hour' },
|
||||||
@@ -63,12 +64,12 @@ const rangeOptions = [
|
|||||||
|
|
||||||
const absoluteFormat = 'MMM D, YYYY HH:mm:ss';
|
const absoluteFormat = 'MMM D, YYYY HH:mm:ss';
|
||||||
|
|
||||||
const rangeIndex = {};
|
const rangeIndex: any = {};
|
||||||
_.each(rangeOptions, frame => {
|
_.each(rangeOptions, (frame: any) => {
|
||||||
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getRelativeTimesList(timepickerSettings, currentDisplay) {
|
export function getRelativeTimesList(timepickerSettings: any, currentDisplay: any) {
|
||||||
const groups = _.groupBy(rangeOptions, (option: any) => {
|
const groups = _.groupBy(rangeOptions, (option: any) => {
|
||||||
option.active = option.display === currentDisplay;
|
option.active = option.display === currentDisplay;
|
||||||
return option.section;
|
return option.section;
|
||||||
@@ -84,7 +85,7 @@ export function getRelativeTimesList(timepickerSettings, currentDisplay) {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date) {
|
function formatDate(date: any) {
|
||||||
return date.format(absoluteFormat);
|
return date.format(absoluteFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +145,12 @@ export function describeTimeRange(range: RawTimeRange): string {
|
|||||||
|
|
||||||
if (moment.isMoment(range.from)) {
|
if (moment.isMoment(range.from)) {
|
||||||
const toMoment = dateMath.parse(range.to, true);
|
const toMoment = dateMath.parse(range.to, true);
|
||||||
return formatDate(range.from) + ' to ' + toMoment.fromNow();
|
return toMoment ? formatDate(range.from) + ' to ' + toMoment.fromNow() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moment.isMoment(range.to)) {
|
if (moment.isMoment(range.to)) {
|
||||||
const from = dateMath.parse(range.from, false);
|
const from = dateMath.parse(range.from, false);
|
||||||
return from.fromNow() + ' to ' + formatDate(range.to);
|
return from ? from.fromNow() + ' to ' + formatDate(range.to) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.to.toString() === 'now') {
|
if (range.to.toString() === 'now') {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { DashNavButton } from './DashNavButton';
|
import { DashNavButton } from './DashNavButton';
|
||||||
|
import { DashNavTimeControls } from './DashNavTimeControls';
|
||||||
import { Tooltip } from '@grafana/ui';
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -16,8 +17,9 @@ import { updateLocation } from 'app/core/actions';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DashboardModel } from '../../state';
|
import { DashboardModel } from '../../state';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface OwnProps {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
editview: string;
|
editview: string;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
@@ -27,6 +29,12 @@ export interface Props {
|
|||||||
onAddPanel: () => void;
|
onAddPanel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StateProps {
|
||||||
|
location: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateProps & OwnProps;
|
||||||
|
|
||||||
export class DashNav extends PureComponent<Props> {
|
export class DashNav extends PureComponent<Props> {
|
||||||
timePickerEl: HTMLElement;
|
timePickerEl: HTMLElement;
|
||||||
timepickerCmp: AngularComponent;
|
timepickerCmp: AngularComponent;
|
||||||
@@ -39,7 +47,6 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const loader = getAngularLoader();
|
const loader = getAngularLoader();
|
||||||
|
|
||||||
const template =
|
const template =
|
||||||
'<gf-time-picker class="gf-timepicker-nav" dashboard="dashboard" ng-if="!dashboard.timepicker.hidden" />';
|
'<gf-time-picker class="gf-timepicker-nav" dashboard="dashboard" ng-if="!dashboard.timepicker.hidden" />';
|
||||||
const scopeProps = { dashboard: this.props.dashboard };
|
const scopeProps = { dashboard: this.props.dashboard };
|
||||||
@@ -161,12 +168,10 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, onAddPanel } = this.props;
|
const { dashboard, onAddPanel, location } = this.props;
|
||||||
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
|
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
|
||||||
const { snapshot } = dashboard;
|
const { snapshot } = dashboard;
|
||||||
|
|
||||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
{this.isInFullscreenOrSettings && this.renderBackButton()}
|
{this.isInFullscreenOrSettings && this.renderBackButton()}
|
||||||
@@ -255,13 +260,20 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
{!dashboard.timepicker.hidden && (
|
||||||
|
<div className="navbar-buttons">
|
||||||
|
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
|
||||||
|
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
location: state.location,
|
||||||
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
updateLocation,
|
updateLocation,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Libaries
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { DashboardModel } from '../../state';
|
||||||
|
import { LocationState } from 'app/types';
|
||||||
|
|
||||||
|
// State
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Utils & Services
|
||||||
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
|
location: LocationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashNavTimeControls extends Component<Props> {
|
||||||
|
timeSrv: TimeSrv = getTimeSrv();
|
||||||
|
|
||||||
|
get refreshParamInUrl(): string {
|
||||||
|
return this.props.location.query.refresh as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeRefreshInterval = (interval: string) => {
|
||||||
|
this.timeSrv.setAutoRefresh(interval);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onRefresh = () => {
|
||||||
|
this.timeSrv.refreshDashboard();
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { dashboard } = this.props;
|
||||||
|
const intervals = dashboard.timepicker.refresh_intervals;
|
||||||
|
return (
|
||||||
|
<RefreshPicker
|
||||||
|
onIntervalChanged={this.onChangeRefreshInterval}
|
||||||
|
onRefresh={this.onRefresh}
|
||||||
|
value={dashboard.refresh}
|
||||||
|
intervals={intervals}
|
||||||
|
tooltip="Refresh dashboard"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,7 +108,7 @@ export class TimePickerCtrl {
|
|||||||
this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString);
|
this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString);
|
||||||
this.refresh = {
|
this.refresh = {
|
||||||
value: this.dashboard.refresh,
|
value: this.dashboard.refresh,
|
||||||
options: _.map(this.panel.refresh_intervals, (interval: any) => {
|
options: this.panel.refresh_intervals.map((interval: any) => {
|
||||||
return { text: interval, value: interval };
|
return { text: interval, value: interval };
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
<div class="navbar-buttons">
|
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
|
||||||
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
|
<i class="fa fa-chevron-left"></i>
|
||||||
<i class="fa fa-chevron-left"></i>
|
</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
|
<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
|
||||||
<i class="fa fa-clock-o"></i>
|
<i class="fa fa-clock-o"></i>
|
||||||
<span ng-bind="ctrl.rangeString"></span>
|
<span ng-bind="ctrl.rangeString"></span>
|
||||||
<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">UTC</span>
|
<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">UTC</span>
|
||||||
<span ng-show="ctrl.dashboard.refresh" class="text-warning"> Refresh every {{ctrl.dashboard.refresh}}</span>
|
<!-- <span ng-show="ctrl.dashboard.refresh" class="text-warning"> Refresh every {{ctrl.dashboard.refresh}}</span> -->
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
|
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
|
||||||
<i class="fa fa-chevron-right"></i>
|
<i class="fa fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
|
<button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
|
||||||
<i class="fa fa-search-minus"></i>
|
<i class="fa fa-search-minus"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
|
<!-- <button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
|
||||||
<i class="fa fa-refresh"></i>
|
<i class="fa fa-refresh"></i>
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
|
<div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
|
||||||
<div class="popover-box">
|
<div class="popover-box">
|
||||||
@@ -75,7 +73,7 @@
|
|||||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="small">Refreshing every:</label>
|
<!-- <label class="small">Refreshing every:</label>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form max-width-28">
|
<div class="gf-form max-width-28">
|
||||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||||
@@ -83,7 +81,7 @@
|
|||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<button type="submit" class="btn gf-form-btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
<button type="submit" class="btn gf-form-btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import templateSrv from 'app/features/templating/template_srv';
|
|||||||
|
|
||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
|
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export class TimeSrv {
|
|||||||
setAutoRefresh(interval) {
|
setAutoRefresh(interval) {
|
||||||
this.dashboard.refresh = interval;
|
this.dashboard.refresh = interval;
|
||||||
this.cancelNextRefresh();
|
this.cancelNextRefresh();
|
||||||
|
|
||||||
if (interval) {
|
if (interval) {
|
||||||
const intervalMs = kbn.interval_to_ms(interval);
|
const intervalMs = kbn.interval_to_ms(interval);
|
||||||
|
|
||||||
@@ -135,15 +136,17 @@ export class TimeSrv {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update url
|
// update url inside timeout to so that a digest happens after (called from react)
|
||||||
const params = this.$location.search();
|
this.$timeout(() => {
|
||||||
if (interval) {
|
const params = this.$location.search();
|
||||||
params.refresh = interval;
|
if (interval) {
|
||||||
this.$location.search(params);
|
params.refresh = interval;
|
||||||
} else if (params.refresh) {
|
this.$location.search(params);
|
||||||
delete params.refresh;
|
} else if (params.refresh) {
|
||||||
this.$location.search(params);
|
delete params.refresh;
|
||||||
}
|
this.$location.search(params);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshDashboard() {
|
refreshDashboard() {
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
const initialQueries: DataQuery[] = ensureQueries(queries);
|
const initialQueries: DataQuery[] = ensureQueries(queries);
|
||||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
||||||
const width = this.el ? this.el.offsetWidth : 0;
|
const width = this.el ? this.el.offsetWidth : 0;
|
||||||
|
|
||||||
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
this.props.initializeExplore(
|
this.props.initializeExplore(
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import { connect } from 'react-redux';
|
|||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
|
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import { DataSourceSelectItem, RawTimeRange, TimeRange } from '@grafana/ui';
|
import { DataSourceSelectItem, RawTimeRange, TimeRange, ClickOutsideWrapper } from '@grafana/ui';
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
|
import {
|
||||||
|
changeDatasource,
|
||||||
|
clearQueries,
|
||||||
|
splitClose,
|
||||||
|
runQueries,
|
||||||
|
splitOpen,
|
||||||
|
changeRefreshInterval,
|
||||||
|
} from './state/actions';
|
||||||
import TimePicker from './TimePicker';
|
import TimePicker from './TimePicker';
|
||||||
import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
|
import { RefreshPicker, SetInterval } from '@grafana/ui';
|
||||||
|
|
||||||
enum IconSide {
|
enum IconSide {
|
||||||
left = 'left',
|
left = 'left',
|
||||||
@@ -51,20 +58,22 @@ interface StateProps {
|
|||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
selectedDatasource: DataSourceSelectItem;
|
selectedDatasource: DataSourceSelectItem;
|
||||||
splitted: boolean;
|
splitted: boolean;
|
||||||
|
refreshInterval: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
changeDatasource: typeof changeDatasource;
|
changeDatasource: typeof changeDatasource;
|
||||||
clearAll: typeof clearQueries;
|
clearAll: typeof clearQueries;
|
||||||
runQuery: typeof runQueries;
|
runQueries: typeof runQueries;
|
||||||
closeSplit: typeof splitClose;
|
closeSplit: typeof splitClose;
|
||||||
split: typeof splitOpen;
|
split: typeof splitOpen;
|
||||||
|
changeRefreshInterval: typeof changeRefreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & OwnProps;
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,23 +86,32 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onRunQuery = () => {
|
onRunQuery = () => {
|
||||||
this.props.runQuery(this.props.exploreId);
|
return this.props.runQueries(this.props.exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCloseTimePicker = () => {
|
onCloseTimePicker = () => {
|
||||||
this.props.timepickerRef.current.setState({ isOpen: false });
|
this.props.timepickerRef.current.setState({ isOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onChangeRefreshInterval = (item: string) => {
|
||||||
|
const { changeRefreshInterval, exploreId } = this.props;
|
||||||
|
changeRefreshInterval(exploreId, item);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
exploreDatasources,
|
exploreDatasources,
|
||||||
|
closeSplit,
|
||||||
exploreId,
|
exploreId,
|
||||||
loading,
|
loading,
|
||||||
range,
|
range,
|
||||||
selectedDatasource,
|
selectedDatasource,
|
||||||
splitted,
|
splitted,
|
||||||
timepickerRef,
|
timepickerRef,
|
||||||
|
refreshInterval,
|
||||||
|
onChangeTime,
|
||||||
|
split,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +127,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{splitted && (
|
{splitted && (
|
||||||
<a className="explore-toolbar-header-close" onClick={() => this.props.closeSplit(exploreId)}>
|
<a className="explore-toolbar-header-close" onClick={() => closeSplit(exploreId)}>
|
||||||
<i className="fa fa-times fa-fw" />
|
<i className="fa fa-times fa-fw" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -133,7 +151,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
{createResponsiveButton({
|
{createResponsiveButton({
|
||||||
splitted,
|
splitted,
|
||||||
title: 'Split',
|
title: 'Split',
|
||||||
onClick: this.props.split,
|
onClick: split,
|
||||||
iconClassName: 'fa fa-fw fa-columns icon-margin-right',
|
iconClassName: 'fa fa-fw fa-columns icon-margin-right',
|
||||||
iconSide: IconSide.left,
|
iconSide: IconSide.left,
|
||||||
})}
|
})}
|
||||||
@@ -141,9 +159,18 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="explore-toolbar-content-item timepicker">
|
<div className="explore-toolbar-content-item timepicker">
|
||||||
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
|
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
|
||||||
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
|
<TimePicker ref={timepickerRef} range={range} onChangeTime={onChangeTime} />
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
|
|
||||||
|
<RefreshPicker
|
||||||
|
onIntervalChanged={this.onChangeRefreshInterval}
|
||||||
|
onRefresh={this.onRunQuery}
|
||||||
|
value={refreshInterval}
|
||||||
|
tooltip="Refresh"
|
||||||
|
/>
|
||||||
|
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="explore-toolbar-content-item">
|
<div className="explore-toolbar-content-item">
|
||||||
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
|
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
|
||||||
Clear All
|
Clear All
|
||||||
@@ -169,7 +196,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
|
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
|
||||||
const splitted = state.explore.split;
|
const splitted = state.explore.split;
|
||||||
const exploreItem = state.explore[exploreId];
|
const exploreItem = state.explore[exploreId];
|
||||||
const { datasourceInstance, datasourceMissing, exploreDatasources, queryTransactions, range } = exploreItem;
|
const {
|
||||||
|
datasourceInstance,
|
||||||
|
datasourceMissing,
|
||||||
|
exploreDatasources,
|
||||||
|
queryTransactions,
|
||||||
|
range,
|
||||||
|
refreshInterval,
|
||||||
|
} = exploreItem;
|
||||||
const selectedDatasource = datasourceInstance
|
const selectedDatasource = datasourceInstance
|
||||||
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -182,13 +216,15 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
range,
|
range,
|
||||||
selectedDatasource,
|
selectedDatasource,
|
||||||
splitted,
|
splitted,
|
||||||
|
refreshInterval,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps: DispatchProps = {
|
const mapDispatchToProps: DispatchProps = {
|
||||||
changeDatasource,
|
changeDatasource,
|
||||||
|
changeRefreshInterval,
|
||||||
clearAll: clearQueries,
|
clearAll: clearQueries,
|
||||||
runQuery: runQueries,
|
runQueries,
|
||||||
closeSplit: splitClose,
|
closeSplit: splitClose,
|
||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,10 +66,19 @@ export interface ChangeTimePayload {
|
|||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChangeRefreshIntervalPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
refreshInterval: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClearQueriesPayload {
|
export interface ClearQueriesPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClearRefreshIntervalPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HighlightLogsExpressionPayload {
|
export interface HighlightLogsExpressionPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
expressions: string[];
|
expressions: string[];
|
||||||
@@ -240,6 +249,13 @@ export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore
|
|||||||
*/
|
*/
|
||||||
export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
|
export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
||||||
|
*/
|
||||||
|
export const changeRefreshIntervalAction = actionCreatorFactory<ChangeRefreshIntervalPayload>(
|
||||||
|
'explore/CHANGE_REFRESH_INTERVAL'
|
||||||
|
).create();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all queries and results.
|
* Clear all queries and results.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
|||||||
const eventBridge = {} as Emitter;
|
const eventBridge = {} as Emitter;
|
||||||
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
|
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
|
||||||
const range = { from: 'now', to: 'now' };
|
const range = { from: 'now', to: 'now' };
|
||||||
const urlState: ExploreUrlState = { datasource: 'some-datasource', queries: [], range, ui };
|
const urlState: ExploreUrlState = {
|
||||||
|
datasource: 'some-datasource',
|
||||||
|
queries: [],
|
||||||
|
range,
|
||||||
|
ui,
|
||||||
|
};
|
||||||
const updateDefaults = makeInitialUpdateState();
|
const updateDefaults = makeInitialUpdateState();
|
||||||
const update = { ...updateDefaults, ...updateOverides };
|
const update = { ...updateDefaults, ...updateOverides };
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@@ -50,6 +55,10 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
|||||||
queries: [] as DataQuery[],
|
queries: [] as DataQuery[],
|
||||||
range,
|
range,
|
||||||
ui,
|
ui,
|
||||||
|
refreshInterval: {
|
||||||
|
label: 'Off',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
import { ResultGetter } from 'app/types/explore';
|
||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import {
|
import {
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
@@ -44,6 +45,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
updateDatasourceInstanceAction,
|
updateDatasourceInstanceAction,
|
||||||
changeQueryAction,
|
changeQueryAction,
|
||||||
|
changeRefreshIntervalAction,
|
||||||
|
ChangeRefreshIntervalPayload,
|
||||||
changeSizeAction,
|
changeSizeAction,
|
||||||
ChangeSizePayload,
|
ChangeSizePayload,
|
||||||
changeTimeAction,
|
changeTimeAction,
|
||||||
@@ -164,7 +167,7 @@ export function changeSize(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
* Change the time range of Explore. Usually called from the Time picker or a graph interaction.
|
||||||
*/
|
*/
|
||||||
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
|
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
@@ -173,6 +176,16 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the refresh interval of Explore. Called from the Refresh picker.
|
||||||
|
*/
|
||||||
|
export function changeRefreshInterval(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
refreshInterval: string
|
||||||
|
): ActionOf<ChangeRefreshIntervalPayload> {
|
||||||
|
return changeRefreshIntervalAction({ exploreId, refreshInterval });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all queries and results.
|
* Clear all queries and results.
|
||||||
*/
|
*/
|
||||||
@@ -526,7 +539,7 @@ export function queryTransactionSuccess(
|
|||||||
/**
|
/**
|
||||||
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||||
*/
|
*/
|
||||||
export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<void> {
|
export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<Promise<any>> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
@@ -543,13 +556,13 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
|
|||||||
|
|
||||||
if (datasourceError) {
|
if (datasourceError) {
|
||||||
// let's not run any queries if data source is in a faulty state
|
// let's not run any queries if data source is in a faulty state
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasNonEmptyQuery(queries)) {
|
if (!hasNonEmptyQuery(queries)) {
|
||||||
dispatch(clearQueriesAction({ exploreId }));
|
dispatch(clearQueriesAction({ exploreId }));
|
||||||
dispatch(stateSave()); // Remember to saves to state and update location
|
dispatch(stateSave()); // Remember to saves to state and update location
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some datasource's query builders allow per-query interval limits,
|
// Some datasource's query builders allow per-query interval limits,
|
||||||
@@ -558,41 +571,46 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
|
|||||||
|
|
||||||
dispatch(runQueriesAction({ exploreId }));
|
dispatch(runQueriesAction({ exploreId }));
|
||||||
// Keep table queries first since they need to return quickly
|
// Keep table queries first since they need to return quickly
|
||||||
if ((ignoreUIState || showingTable) && supportsTable) {
|
const tableQueriesPromise =
|
||||||
dispatch(
|
(ignoreUIState || showingTable) && supportsTable
|
||||||
runQueriesForType(
|
? dispatch(
|
||||||
exploreId,
|
runQueriesForType(
|
||||||
'Table',
|
exploreId,
|
||||||
{
|
'Table',
|
||||||
interval,
|
{
|
||||||
format: 'table',
|
interval,
|
||||||
instant: true,
|
format: 'table',
|
||||||
valueWithRefId: true,
|
instant: true,
|
||||||
},
|
valueWithRefId: true,
|
||||||
(data: any) => data[0]
|
},
|
||||||
)
|
(data: any[]) => data[0]
|
||||||
);
|
)
|
||||||
}
|
)
|
||||||
if ((ignoreUIState || showingGraph) && supportsGraph) {
|
: undefined;
|
||||||
dispatch(
|
const typeQueriesPromise =
|
||||||
runQueriesForType(
|
(ignoreUIState || showingGraph) && supportsGraph
|
||||||
exploreId,
|
? dispatch(
|
||||||
'Graph',
|
runQueriesForType(
|
||||||
{
|
exploreId,
|
||||||
interval,
|
'Graph',
|
||||||
format: 'time_series',
|
{
|
||||||
instant: false,
|
interval,
|
||||||
maxDataPoints: containerWidth,
|
format: 'time_series',
|
||||||
},
|
instant: false,
|
||||||
makeTimeSeriesList
|
maxDataPoints: containerWidth,
|
||||||
)
|
},
|
||||||
);
|
makeTimeSeriesList
|
||||||
}
|
)
|
||||||
if ((ignoreUIState || showingLogs) && supportsLogs) {
|
)
|
||||||
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
: undefined;
|
||||||
}
|
const logsQueriesPromise =
|
||||||
|
(ignoreUIState || showingLogs) && supportsLogs
|
||||||
|
? dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
|
|
||||||
|
return Promise.all([tableQueriesPromise, typeQueriesPromise, logsQueriesPromise]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,14 +625,13 @@ function runQueriesForType(
|
|||||||
exploreId: ExploreId,
|
exploreId: ExploreId,
|
||||||
resultType: ResultType,
|
resultType: ResultType,
|
||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
resultGetter?: any
|
resultGetter?: ResultGetter
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
|
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
|
||||||
const datasourceId = datasourceInstance.meta.id;
|
const datasourceId = datasourceInstance.meta.id;
|
||||||
|
|
||||||
// Run all queries concurrently
|
// Run all queries concurrently
|
||||||
queries.forEach(async (query, rowIndex) => {
|
const queryPromises = queries.map(async (query, rowIndex) => {
|
||||||
const transaction = buildQueryTransaction(
|
const transaction = buildQueryTransaction(
|
||||||
query,
|
query,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
@@ -638,6 +655,8 @@ function runQueriesForType(
|
|||||||
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
|
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Promise.all(queryPromises);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +833,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
const { datasource, queries, range, ui } = urlState;
|
const { datasource, queries, range, ui } = urlState;
|
||||||
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
|
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
|
||||||
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
||||||
|
|
||||||
// need to refresh datasource
|
// need to refresh datasource
|
||||||
if (update.datasource) {
|
if (update.datasource) {
|
||||||
const initialQueries = ensureQueries(queries);
|
const initialQueries = ensureQueries(queries);
|
||||||
|
|||||||
@@ -515,6 +515,44 @@ describe('Explore reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('and refreshInterval differs', () => {
|
||||||
|
it('then it should return update refreshInterval', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
update: {
|
||||||
|
...initalState.left.update,
|
||||||
|
refreshInterval: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const stateWithDifferentDataSource = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
urlState: {
|
||||||
|
...initalState.left.urlState,
|
||||||
|
refreshInterval: '5s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithDifferentDataSource)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('and nothing differs', () => {
|
describe('and nothing differs', () => {
|
||||||
fit('then it should return update ui', () => {
|
fit('then it should return update ui', () => {
|
||||||
const { initalState, serializedUrlState } = setup();
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
changeQueryAction,
|
changeQueryAction,
|
||||||
changeSizeAction,
|
changeSizeAction,
|
||||||
changeTimeAction,
|
changeTimeAction,
|
||||||
|
changeRefreshIntervalAction,
|
||||||
clearQueriesAction,
|
clearQueriesAction,
|
||||||
highlightLogsExpressionAction,
|
highlightLogsExpressionAction,
|
||||||
initializeExploreAction,
|
initializeExploreAction,
|
||||||
@@ -67,6 +68,7 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({
|
|||||||
range: false,
|
range: false,
|
||||||
ui: false,
|
ui: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a fresh Explore area state
|
* Returns a fresh Explore area state
|
||||||
*/
|
*/
|
||||||
@@ -101,10 +103,11 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
|||||||
/**
|
/**
|
||||||
* Global Explore state that handles multiple Explore areas and the split state
|
* Global Explore state that handles multiple Explore areas and the split state
|
||||||
*/
|
*/
|
||||||
|
export const initialExploreItemState = makeExploreItemState();
|
||||||
export const initialExploreState: ExploreState = {
|
export const initialExploreState: ExploreState = {
|
||||||
split: null,
|
split: null,
|
||||||
left: makeExploreItemState(),
|
left: initialExploreItemState,
|
||||||
right: makeExploreItemState(),
|
right: initialExploreItemState,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,6 +178,16 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
return { ...state, range: action.payload.range };
|
return { ...state, range: action.payload.range };
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: changeRefreshIntervalAction,
|
||||||
|
mapper: (state, action): ExploreItemState => {
|
||||||
|
const { refreshInterval } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
refreshInterval: refreshInterval,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: clearQueriesAction,
|
filter: clearQueriesAction,
|
||||||
mapper: (state): ExploreItemState => {
|
mapper: (state): ExploreItemState => {
|
||||||
@@ -580,7 +593,11 @@ export const updateChildRefreshState = (
|
|||||||
const urlState = parseUrlState(queryState);
|
const urlState = parseUrlState(queryState);
|
||||||
if (!state.urlState || path !== '/explore') {
|
if (!state.urlState || path !== '/explore') {
|
||||||
// we only want to refresh when browser back/forward
|
// we only want to refresh when browser back/forward
|
||||||
return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
|
return {
|
||||||
|
...state,
|
||||||
|
urlState,
|
||||||
|
update: { datasource: false, queries: false, range: false, ui: false },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
|
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
|
||||||
|
|||||||
@@ -84,12 +84,22 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="gf-form-select-box__control--menu-right"
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
components={
|
||||||
|
Object {
|
||||||
|
"Group": [Function],
|
||||||
|
"IndicatorsContainer": [Function],
|
||||||
|
"MenuList": [Function],
|
||||||
|
"Option": [Function],
|
||||||
|
"SingleValue": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
maxMenuHeight={300}
|
maxMenuHeight={300}
|
||||||
|
menuIsOpen={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
openMenuOnFocus={false}
|
openMenuOnFocus={false}
|
||||||
options={
|
options={
|
||||||
@@ -160,12 +170,22 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="gf-form-select-box__control--menu-right"
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
components={
|
||||||
|
Object {
|
||||||
|
"Group": [Function],
|
||||||
|
"IndicatorsContainer": [Function],
|
||||||
|
"MenuList": [Function],
|
||||||
|
"Option": [Function],
|
||||||
|
"SingleValue": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
maxMenuHeight={300}
|
maxMenuHeight={300}
|
||||||
|
menuIsOpen={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
openMenuOnFocus={false}
|
openMenuOnFocus={false}
|
||||||
options={
|
options={
|
||||||
|
|||||||
@@ -13,86 +13,88 @@ Array [
|
|||||||
>
|
>
|
||||||
Aggregation
|
Aggregation
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
|
||||||
Select Reducer
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__input"
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
style={
|
>
|
||||||
Object {
|
Select Reducer
|
||||||
"display": "inline-block",
|
</div>
|
||||||
}
|
<div
|
||||||
}
|
className="css-0"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-2-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,41 +13,43 @@ Array [
|
|||||||
>
|
>
|
||||||
Service
|
Service
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
>
|
||||||
Select Services
|
<div
|
||||||
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
|
>
|
||||||
|
Select Services
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="css-14uuagi"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-2-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
readOnly={true}
|
||||||
|
tabIndex="0"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0 gf-form-select-box__indicators"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="gf-form-select-box__select-arrow "
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
className="css-14uuagi"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-2-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
readOnly={true}
|
|
||||||
tabIndex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0 gf-form-select-box__indicators"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="gf-form-select-box__select-arrow "
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,86 +73,88 @@ Array [
|
|||||||
>
|
>
|
||||||
Metric
|
Metric
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
|
||||||
Select Metric
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__input"
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
style={
|
>
|
||||||
Object {
|
Select Metric
|
||||||
"display": "inline-block",
|
</div>
|
||||||
}
|
<div
|
||||||
}
|
className="css-0"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-3-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-3-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,86 +185,88 @@ Array [
|
|||||||
>
|
>
|
||||||
Aggregation
|
Aggregation
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__placeholder"
|
className="css-0 gf-form-select-box__value-container"
|
||||||
>
|
|
||||||
Select Reducer
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__input"
|
className="css-0 gf-form-select-box__placeholder"
|
||||||
style={
|
>
|
||||||
Object {
|
Select Reducer
|
||||||
"display": "inline-block",
|
</div>
|
||||||
}
|
<div
|
||||||
}
|
className="css-0"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-4-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-4-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,90 +299,92 @@ Array [
|
|||||||
>
|
>
|
||||||
Alignment Period
|
Alignment Period
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div>
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__control"
|
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||||
onMouseDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onTouchEnd={[Function]}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
|
className="css-0 gf-form-select-box__control"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-select-box__single-value"
|
className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__img-value"
|
className="css-0 gf-form-select-box__single-value"
|
||||||
>
|
>
|
||||||
stackdriver auto
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form-select-box__input"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "inline-block",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={false}
|
|
||||||
id="react-select-5-input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
spellCheck="false"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"background": 0,
|
|
||||||
"border": 0,
|
|
||||||
"boxSizing": "content-box",
|
|
||||||
"color": "inherit",
|
|
||||||
"fontSize": "inherit",
|
|
||||||
"opacity": 1,
|
|
||||||
"outline": 0,
|
|
||||||
"padding": 0,
|
|
||||||
"width": "1px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabIndex="0"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
|
className="gf-form-select-box__img-value"
|
||||||
|
>
|
||||||
|
stackdriver auto
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="css-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-box__input"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": 0,
|
"display": "inline-block",
|
||||||
"left": 0,
|
|
||||||
"overflow": "scroll",
|
|
||||||
"position": "absolute",
|
|
||||||
"top": 0,
|
|
||||||
"visibility": "hidden",
|
|
||||||
"whiteSpace": "pre",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={false}
|
||||||
|
id="react-select-5-input"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
spellCheck="false"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": 0,
|
||||||
|
"border": 0,
|
||||||
|
"boxSizing": "content-box",
|
||||||
|
"color": "inherit",
|
||||||
|
"fontSize": "inherit",
|
||||||
|
"opacity": 1,
|
||||||
|
"outline": 0,
|
||||||
|
"padding": 0,
|
||||||
|
"width": "1px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 0,
|
||||||
|
"left": 0,
|
||||||
|
"overflow": "scroll",
|
||||||
|
"position": "absolute",
|
||||||
|
"top": 0,
|
||||||
|
"visibility": "hidden",
|
||||||
|
"whiteSpace": "pre",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className="css-0 gf-form-select-box__indicators"
|
||||||
className="css-0 gf-form-select-box__indicators"
|
>
|
||||||
>
|
<span
|
||||||
<span
|
className="gf-form-select-box__select-arrow "
|
||||||
className="gf-form-select-box__select-arrow "
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -250,6 +250,11 @@ export interface ExploreItemState {
|
|||||||
*/
|
*/
|
||||||
hiddenLogLevels?: LogLevel[];
|
hiddenLogLevels?: LogLevel[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often query should be refreshed
|
||||||
|
*/
|
||||||
|
refreshInterval?: string;
|
||||||
|
|
||||||
urlState: ExploreUrlState;
|
urlState: ExploreUrlState;
|
||||||
|
|
||||||
update: ExploreUpdateState;
|
update: ExploreUpdateState;
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ $zindex-tooltip: 1030;
|
|||||||
$zindex-modal-backdrop: 1040;
|
$zindex-modal-backdrop: 1040;
|
||||||
$zindex-modal: 1050;
|
$zindex-modal: 1050;
|
||||||
$zindex-typeahead: 1060;
|
$zindex-typeahead: 1060;
|
||||||
|
$zindex-timepicker-popover: 1070;
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -50,6 +50,16 @@
|
|||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
@include box-shadow(none);
|
@include box-shadow(none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--radius-left-0 {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--radius-right-0 {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button Sizes
|
// Button Sizes
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
.navbar-button--share,
|
.navbar-button--share,
|
||||||
.navbar-button--settings,
|
.navbar-button--settings,
|
||||||
.navbar-page-btn .fa-caret-down,
|
.navbar-page-btn .fa-caret-down,
|
||||||
|
.refresh-picker,
|
||||||
.gf-timepicker-nav {
|
.gf-timepicker-nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -135,6 +136,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--refresh {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--attached {
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
&--tight {
|
&--tight {
|
||||||
padding: 7px 4px;
|
padding: 7px 4px;
|
||||||
|
|
||||||
|
|||||||
22
yarn.lock
22
yarn.lock
@@ -7967,6 +7967,13 @@ get-stream@^4.0.0, get-stream@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pump "^3.0.0"
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
get-user-locale@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-1.1.1.tgz#edff0a8bbd6aa3ed0ca30cc441e1acd111543b7f"
|
||||||
|
integrity sha512-KuA+vMhsY+rSPK8hrmOvf7xXIMTs+L06RkgZ83jawZHSEqPLafZtQ63d3waXW3r8z6EQ49I/trraNncWM+s/2g==
|
||||||
|
dependencies:
|
||||||
|
lodash.once "^4.1.1"
|
||||||
|
|
||||||
get-value@^2.0.3, get-value@^2.0.6:
|
get-value@^2.0.3, get-value@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||||
@@ -11229,6 +11236,11 @@ meow@^3.3.0, meow@^3.7.0:
|
|||||||
redent "^1.0.0"
|
redent "^1.0.0"
|
||||||
trim-newlines "^1.0.0"
|
trim-newlines "^1.0.0"
|
||||||
|
|
||||||
|
merge-class-names@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.1.1.tgz#3bd2f38eb5418c464a0fef615484fdf6c8932256"
|
||||||
|
integrity sha512-+UUWBUoFw9QLY/UlBKU/xk9h6OhyG3BUDDuF2eIJcxmusWb/uedvNpZGkysqMw5b/ds+wkX7NJTDSdUuRsCNyA==
|
||||||
|
|
||||||
merge-deep@^3.0.2:
|
merge-deep@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2"
|
resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2"
|
||||||
@@ -13930,6 +13942,16 @@ react-addons-create-fragment@^15.5.3:
|
|||||||
loose-envify "^1.3.1"
|
loose-envify "^1.3.1"
|
||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
|
|
||||||
|
react-calendar@^2.18.1:
|
||||||
|
version "2.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.18.1.tgz#f8ef9468d8566aa0d47d9d70c88917bb2030bcb9"
|
||||||
|
integrity sha512-J3tVim1gLpnsCOaeez+z4QJB5oK6UYLJj5TSMOStSJBvkWMEcTzj7bq7yCJJCNLUg2Vd3i11gJXish0LUFhXaw==
|
||||||
|
dependencies:
|
||||||
|
get-user-locale "^1.1.1"
|
||||||
|
merge-class-names "^1.1.1"
|
||||||
|
prop-types "^15.6.0"
|
||||||
|
react-lifecycles-compat "^3.0.4"
|
||||||
|
|
||||||
react-clientside-effect@^1.2.0:
|
react-clientside-effect@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.0.tgz#db823695f75e9616a5e4dd6d908e5ea627fb2516"
|
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.0.tgz#db823695f75e9616a5e4dd6d908e5ea627fb2516"
|
||||||
|
|||||||
Reference in New Issue
Block a user