Commit 0412a28d by Johannes Schill Committed by Torkel Ödegaard

TimePicker: New time picker dropdown & custom range UI (#16811)

* feat: Add new picker to DashNavTimeControls

* chore: noImplicitAny limit reached

* chore: noImplicityAny fix

* chore: Add momentUtc helper to avoid the isUtc conditionals

* chore: Move getRaw from Explore's time picker to grafana/ui utils and rename to getRawRange

* feat: Use helper functions to convert utc to browser time

* fix: Dont Select current value when pressing tab when using Time Picker

* fix: Add tabIndex to time range inputs so tab works smoothly and prevent mouseDown event to propagate to react-select

* fix: Add spacing to custom range labels

* fix: Updated snapshot

* fix: Re-adding getRaw() temporary to fix the build

* fix: Disable scroll event in Popper when we're using the TimePicker so the popup wont "follow" the menu

* fix: Move all "Last xxxx" quick ranges to the menu and show a "UTC" text when applicable

* fix: Add zoom functionality

* feat: Add logic to mark selected option as active

* fix: Add tooltip to zoom button

* fix: lint fix after rebase

* chore: Remove old time picker from DashNav

* TimePicker: minor design update

* chore: Move all time picker quick ranges to the menu

* fix: Remove the popover border-right, since the quick ranges are gone

* chore: Remove function not in use

* Fix: Close time picker on resize event

* Fix: Remove border bottom

* Fix: Use fa icons on prev/next arrows

* Fix: Pass ref from TimePicker to TimePickerOptionGroup so the popover will align as it should

* Fix: time picker ui adjustments to get better touch area on buttons

* Fix: Dont increase line height on large screens

* TimePicker: style updates

* Fix: Add more prominent colors for selected dates and fade out dates in previous/next month

* TimePicker: style updates2

* TimePicker: Big refactorings and style changes

* Removed use of Popper not sure we need that here?
* Made active selected item in the list have the "selected" checkmark
* Changed design of popover
* Changed design of and implementation of the Custom selection in the dropdown it did not feel like a item you
could select like the rest now the list is just a normal list

* TimePicker: Refactoring & style changes

* TimePicker: use same date format everywhere

* TimePicker: Calendar style updates

* TimePicker: fixed unit test

* fixed unit test

* TimeZone: refactoring time zone type

* TimePicker: refactoring

* TimePicker: finally to UTC to work

* TimePicker: better way to handle calendar utc dates

* TimePicker: Fixed tooltip issues

* Updated snapshot

* TimePicker: moved tooltip from DashNavControls into TimePicker
parent 0adbb001
import React, { PureComponent } from 'react';
import React, { PureComponent, ReactElement } from 'react';
import Select, { SelectOptionItem } from './Select';
import { PopperContent } from '../Tooltip/PopperController';
interface ButtonComponentProps {
label: string | undefined;
label: ReactElement | string | undefined;
className: string | undefined;
iconClass?: string;
}
......@@ -21,7 +21,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
<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" />
{!props.menuIsOpen && <i className="fa fa-caret-down fa-fw" />}
{props.menuIsOpen && <i className="fa fa-caret-up fa-fw" />}
</div>
</button>
);
......@@ -30,8 +31,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
export interface Props<T> {
className: string | undefined;
options: Array<SelectOptionItem<T>>;
value: SelectOptionItem<T>;
label?: string;
value?: SelectOptionItem<T>;
label?: ReactElement | string;
iconClass?: string;
components?: any;
maxMenuHeight?: number;
......@@ -40,6 +41,7 @@ export interface Props<T> {
isMenuOpen?: boolean;
onOpenMenu?: () => void;
onCloseMenu?: () => void;
tabSelectsValue?: boolean;
}
export class ButtonSelect<T> extends PureComponent<Props<T>> {
......@@ -61,6 +63,7 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
isMenuOpen,
onOpenMenu,
onCloseMenu,
tabSelectsValue,
} = this.props;
const combinedComponents = {
...components,
......@@ -75,13 +78,14 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
options={options}
onChange={this.onChange}
value={value}
isOpen={isMenuOpen}
onOpenMenu={onOpenMenu}
onCloseMenu={onCloseMenu}
maxMenuHeight={maxMenuHeight}
components={combinedComponents}
className="gf-form-select-box-button-select"
tooltipContent={tooltipContent}
isOpen={isMenuOpen}
onOpenMenu={onOpenMenu}
onCloseMenu={onCloseMenu}
tabSelectsValue={tabSelectsValue}
/>
);
}
......
......@@ -53,6 +53,7 @@ export interface CommonProps<T> {
tooltipContent?: PopperContent<any>;
onOpenMenu?: () => void;
onCloseMenu?: () => void;
tabSelectsValue?: boolean;
}
export interface SelectProps<T> extends CommonProps<T> {
......@@ -65,26 +66,6 @@ interface AsyncProps<T> extends CommonProps<T> {
loadingMessage?: () => string;
}
const wrapInTooltip = (
component: React.ReactElement,
tooltipContent: PopperContent<any> | undefined,
isMenuOpen: boolean | undefined
) => {
const showTooltip = isMenuOpen ? false : undefined;
if (tooltipContent) {
return (
<Tooltip show={showTooltip} content={tooltipContent} placement="bottom">
<div>
{/* div needed for tooltip */}
{component}
</div>
</Tooltip>
);
} else {
return <div>{component}</div>;
}
};
export const MenuList = (props: any) => {
return (
<components.MenuList {...props}>
......@@ -107,6 +88,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
isLoading: false,
backspaceRemovesValue: true,
maxMenuHeight: 300,
tabSelectsValue: true,
components: {
Option: SelectOption,
SingleValue,
......@@ -116,20 +98,6 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
},
};
onOpenMenu = () => {
const { onOpenMenu } = this.props;
if (onOpenMenu) {
onOpenMenu();
}
};
onCloseMenu = () => {
const { onCloseMenu } = this.props;
if (onCloseMenu) {
onCloseMenu();
}
};
render() {
const {
defaultValue,
......@@ -155,6 +123,9 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
isOpen,
components,
tooltipContent,
tabSelectsValue,
onCloseMenu,
onOpenMenu,
} = this.props;
let widthClass = '';
......@@ -164,37 +135,43 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
const selectComponents = { ...Select.defaultProps.components, ...components };
return wrapInTooltip(
<ReactSelect
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={selectComponents}
defaultValue={defaultValue}
value={value}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
menuShouldScrollIntoView={false}
isSearchable={isSearchable}
onChange={onChange}
options={options}
placeholder={placeholder || 'Choose'}
styles={resetSelectStyles()}
isDisabled={isDisabled}
isLoading={isLoading}
isClearable={isClearable}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={maxMenuHeight}
noOptionsMessage={noOptionsMessage}
isMulti={isMulti}
backspaceRemovesValue={backspaceRemovesValue}
menuIsOpen={isOpen}
onMenuOpen={this.onOpenMenu}
onMenuClose={this.onCloseMenu}
/>,
tooltipContent,
isOpen
return (
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
{(onOpenMenuInternal, onCloseMenuInternal) => {
return (
<ReactSelect
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={selectComponents}
defaultValue={defaultValue}
value={value}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
menuShouldScrollIntoView={false}
isSearchable={isSearchable}
onChange={onChange}
options={options}
placeholder={placeholder || 'Choose'}
styles={resetSelectStyles()}
isDisabled={isDisabled}
isLoading={isLoading}
isClearable={isClearable}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={maxMenuHeight}
noOptionsMessage={noOptionsMessage}
isMulti={isMulti}
backspaceRemovesValue={backspaceRemovesValue}
menuIsOpen={isOpen}
onMenuOpen={onOpenMenuInternal}
onMenuClose={onCloseMenuInternal}
tabSelectsValue={tabSelectsValue}
/>
);
}}
</WrapInTooltip>
);
}
}
......@@ -239,6 +216,9 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
maxMenuHeight,
isMulti,
tooltipContent,
onCloseMenu,
onOpenMenu,
isOpen,
} = this.props;
let widthClass = '';
......@@ -248,43 +228,105 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
return wrapInTooltip(
<ReactAsyncSelect
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={{
Option: SelectOption,
SingleValue,
IndicatorsContainer,
NoOptionsMessage,
return (
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
{(onOpenMenuInternal, onCloseMenuInternal) => {
return (
<ReactAsyncSelect
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={{
Option: SelectOption,
SingleValue,
IndicatorsContainer,
NoOptionsMessage,
}}
defaultValue={defaultValue}
value={value}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
menuShouldScrollIntoView={false}
onChange={onChange}
loadOptions={loadOptions}
isLoading={isLoading}
defaultOptions={defaultOptions}
placeholder={placeholder || 'Choose'}
styles={resetSelectStyles()}
loadingMessage={loadingMessage}
noOptionsMessage={noOptionsMessage}
isDisabled={isDisabled}
isSearchable={isSearchable}
isClearable={isClearable}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={maxMenuHeight}
isMulti={isMulti}
backspaceRemovesValue={backspaceRemovesValue}
/>
);
}}
defaultValue={defaultValue}
value={value}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
menuShouldScrollIntoView={false}
onChange={onChange}
loadOptions={loadOptions}
isLoading={isLoading}
defaultOptions={defaultOptions}
placeholder={placeholder || 'Choose'}
styles={resetSelectStyles()}
loadingMessage={loadingMessage}
noOptionsMessage={noOptionsMessage}
isDisabled={isDisabled}
isSearchable={isSearchable}
isClearable={isClearable}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={maxMenuHeight}
isMulti={isMulti}
backspaceRemovesValue={backspaceRemovesValue}
/>,
tooltipContent,
false
</WrapInTooltip>
);
}
}
export interface TooltipWrapperProps {
children: (onOpenMenu: () => void, onCloseMenu: () => void) => React.ReactNode;
onOpenMenu?: () => void;
onCloseMenu?: () => void;
isOpen?: boolean;
tooltipContent?: PopperContent<any>;
}
export interface TooltipWrapperState {
isOpenInternal: boolean;
}
export class WrapInTooltip extends PureComponent<TooltipWrapperProps, TooltipWrapperState> {
state: TooltipWrapperState = {
isOpenInternal: false,
};
onOpenMenu = () => {
const { onOpenMenu } = this.props;
if (onOpenMenu) {
onOpenMenu();
}
this.setState({ isOpenInternal: true });
};
onCloseMenu = () => {
const { onCloseMenu } = this.props;
if (onCloseMenu) {
onCloseMenu();
}
this.setState({ isOpenInternal: false });
};
render() {
const { children, isOpen, tooltipContent } = this.props;
const { isOpenInternal } = this.state;
let showTooltip: boolean | undefined = undefined;
if (isOpenInternal || isOpen) {
showTooltip = false;
}
if (tooltipContent) {
return (
<Tooltip show={showTooltip} content={tooltipContent} placement="bottom">
<div>
{/* div needed for tooltip */}
{children(this.onOpenMenu, this.onCloseMenu)}
</div>
</Tooltip>
);
} else {
return <div>{children(this.onOpenMenu, this.onCloseMenu)}</div>;
}
}
}
export default Select;
......@@ -53,7 +53,7 @@ $select-input-bg-disabled: $input-bg-disabled;
}
.gf-form-select-box__menu {
background: $input-bg;
background: $menu-dropdown-bg;
box-shadow: $menu-dropdown-shadow;
position: absolute;
z-index: $zindex-dropdown;
......
......@@ -9,168 +9,6 @@ import { TimeFragment } from '../../types/time';
import { dateTime } from '../../utils/moment_wrapper';
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);
......@@ -186,20 +24,18 @@ TimePickerStories.add('default', () => {
{(value, updateValue) => {
return (
<TimePicker
isTimezoneUtc={false}
timeZone="browser"
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 },
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
]}
popoverOptions={popoverOptions}
onChange={timeRange => {
action('onChange fired')(timeRange);
updateValue(timeRange);
......
......@@ -16,7 +16,7 @@ TimePickerCalendarStories.add('default', () => (
{(value, updateValue) => {
return (
<TimePickerCalendar
isTimezoneUtc={false}
timeZone="browser"
value={value}
onChange={timeRange => {
action('onChange fired')(timeRange);
......
import React, { PureComponent } from 'react';
import Calendar from 'react-calendar/dist/entry.nostyle';
import { TimeFragment } from '../../types/time';
import { Timezone } from '../../utils/datemath';
import { DateTime, dateTime, isDateTime } from '../../utils/moment_wrapper';
import { TimeFragment, TimeZone, TIME_FORMAT } from '../../types/time';
import { DateTime, dateTime, toUtc } from '../../utils/moment_wrapper';
import { stringToDateTimeType } from './time';
export interface Props {
value: TimeFragment;
isTimezoneUtc: boolean;
roundup?: boolean;
timezone?: Timezone;
timeZone?: TimeZone;
onChange: (value: DateTime) => void;
}
export class TimePickerCalendar extends PureComponent<Props> {
onCalendarChange = (date: Date | Date[]) => {
const { onChange } = this.props;
const { onChange, timeZone } = this.props;
if (Array.isArray(date)) {
return;
}
onChange(dateTime(date));
let newDate = dateTime(date);
if (timeZone === 'utc') {
newDate = toUtc(newDate.format(TIME_FORMAT));
}
onChange(newDate);
};
render() {
const { value, isTimezoneUtc, roundup, timezone } = this.props;
const dateValue = isDateTime(value)
? value.toDate()
: stringToDateTimeType(value, isTimezoneUtc, roundup, timezone).toDate();
const calendarValue = dateValue instanceof Date && !isNaN(dateValue.getTime()) ? dateValue : dateTime().toDate();
const { value, roundup, timeZone } = this.props;
let date = stringToDateTimeType(value, roundup, timeZone);
if (!date.isValid()) {
date = dateTime();
}
return (
<Calendar
value={calendarValue}
value={date.toDate()}
next2Label={null}
prev2Label={null}
className="time-picker-calendar"
tileClassName="time-picker-calendar-tile"
onChange={this.onCalendarChange}
nextLabel={<span className="fa fa-angle-right" />}
prevLabel={<span className="fa fa-angle-left" />}
/>
);
}
......
import React, { PureComponent, ChangeEvent } from 'react';
import { TimeFragment, TIME_FORMAT } from '../../types/time';
import { TimeFragment, TIME_FORMAT, TimeZone } from '../../types/time';
import { Input } from '../Input/Input';
import { stringToDateTimeType, isValidTimeString } from './time';
import { isDateTime } from '../../utils/moment_wrapper';
export interface Props {
value: TimeFragment;
isTimezoneUtc: boolean;
roundup?: boolean;
timezone?: string;
timeZone?: TimeZone;
onChange: (value: string, isValid: boolean) => void;
tabIndex?: number;
}
export class TimePickerInput extends PureComponent<Props> {
isValid = (value: string) => {
const { isTimezoneUtc } = this.props;
const { timeZone, roundup } = this.props;
if (value.indexOf('now') !== -1) {
const isValid = isValidTimeString(value);
return isValid;
}
const parsed = stringToDateTimeType(value, isTimezoneUtc);
const parsed = stringToDateTimeType(value, roundup, timeZone);
const isValid = parsed.isValid();
return isValid;
};
......@@ -42,7 +42,7 @@ export class TimePickerInput extends PureComponent<Props> {
};
render() {
const { value } = this.props;
const { value, tabIndex } = this.props;
const valueString = this.valueToString(value);
const error = !this.isValid(valueString);
......@@ -54,6 +54,7 @@ export class TimePickerInput extends PureComponent<Props> {
hideErrorMessage={true}
value={valueString}
className={`time-picker-input${error ? '-error' : ''}`}
tabIndex={tabIndex}
/>
);
}
......
import React, { ComponentType } from 'react';
import { storiesOf } from '@storybook/react';
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';
import { dateTime } from '../../utils/moment_wrapper';
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: dateTime(), to: dateTime(), 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}
/>
));
import React, { PureComponent, createRef } from 'react';
import { GroupProps } from 'react-select/lib/components/Group';
import { Props as TimePickerProps, TimePickerPopover } from './TimePickerPopover';
import { TimeRange } from '../../types/time';
import { Popper } from '../Tooltip/Popper';
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>
</>
);
}
}
......@@ -5,7 +5,6 @@ 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';
import { dateTime, DateTime } from '../../utils/moment_wrapper';
const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module);
......@@ -24,12 +23,11 @@ TimePickerPopoverStories.add('default', () => (
return (
<TimePickerPopover
value={value}
isTimezoneUtc={false}
timeZone="browser"
onChange={timeRange => {
action('onChange fired')(timeRange);
updateValue(timeRange);
}}
options={popoverOptions}
/>
);
}}
......
import React, { Component, SyntheticEvent } from 'react';
import { TimeRange, TimeOptions, TimeOption } from '../../types/time';
// Libraries
import React, { Component } from 'react';
// Components
import { TimePickerCalendar } from './TimePickerCalendar';
import { TimePickerInput } from './TimePickerInput';
import { mapTimeOptionToTimeRange } from './time';
import { Timezone } from '../../utils/datemath';
import { rawToTimeRange } from './time';
// Types
import { DateTime } from '../../utils/moment_wrapper';
import { TimeRange, TimeZone } from '../../types/time';
export interface Props {
value: TimeRange;
options: TimeOptions;
isTimezoneUtc: boolean;
timezone?: Timezone;
onChange?: (timeRange: TimeRange) => void;
timeZone?: TimeZone;
onChange: (timeRange: TimeRange) => void;
}
export interface State {
value: TimeRange;
from: DateTime | string;
to: DateTime | string;
isFromInputValid: boolean;
isToInputValid: boolean;
}
export class TimePickerPopover extends Component<Props, State> {
static popoverClassName = 'time-picker-popover';
constructor(props: Props) {
super(props);
this.state = { value: props.value, isFromInputValid: true, isToInputValid: true };
this.state = {
from: props.value.raw.from,
to: props.value.raw.to,
isFromInputValid: true,
isToInputValid: true,
};
}
onFromInputChanged = (value: string, valid: boolean) => {
this.setState({
value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
isFromInputValid: valid,
});
this.setState({ from: value, isFromInputValid: valid });
};
onToInputChanged = (value: string, valid: boolean) => {
this.setState({
value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } },
isToInputValid: valid,
});
this.setState({ to: value, isToInputValid: valid });
};
onFromCalendarChanged = (value: DateTime) => {
this.setState({
value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
});
this.setState({ from: value });
};
onToCalendarChanged = (value: DateTime) => {
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));
}
this.setState({ to: value });
};
onApplyClick = () => {
const { onChange } = this.props;
if (onChange) {
onChange(this.state.value);
}
const { onChange, timeZone } = this.props;
const { from, to } = this.state;
onChange(rawToTimeRange({ from, to }, timeZone));
};
render() {
const { options, isTimezoneUtc, timezone } = this.props;
const { isFromInputValid, isToInputValid, value } = this.state;
const { timeZone } = this.props;
const { isFromInputValid, isToInputValid, from, to } = this.state;
const isValid = isFromInputValid && isToInputValid;
return (
<div className={TimePickerPopover.popoverClassName}>
<div className="time-picker-popover-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>
<div className="time-picker-popover-body">
<div className="time-picker-popover-body-custom-ranges">
<div className="time-picker-popover-body-custom-ranges-input">
<div className="gf-form">
<label className="gf-form-label">From</label>
<TimePickerInput
isTimezoneUtc={isTimezoneUtc}
roundup={false}
timezone={timezone}
value={value.raw.from}
timeZone={timeZone}
value={from}
onChange={this.onFromInputChanged}
tabIndex={1}
/>
</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>
<div className="time-picker-popover-body-custom-ranges-calendar">
<TimePickerCalendar
timeZone={timeZone}
roundup={false}
value={from}
onChange={this.onFromCalendarChanged}
/>
</div>
</div>
<div className="time-picker-popover-body-custom-ranges">
<div className="time-picker-popover-body-custom-ranges-input">
<div className="gf-form">
<label className="gf-form-label">To</label>
<TimePickerInput
isTimezoneUtc={isTimezoneUtc}
roundup={true}
timezone={timezone}
value={value.raw.to}
timeZone={timeZone}
value={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}
tabIndex={2}
/>
</div>
</div>
<div className="time-picker-popover-body-custom-ranges-calendar">
<TimePickerCalendar roundup={true} timeZone={timeZone} value={to} onChange={this.onToCalendarChanged} />
</div>
</div>
<div 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 className="time-picker-popover-footer">
<button type="submit" className="btn btn-success" disabled={!isValid} onClick={this.onApplyClick}>
Apply
</button>
</div>
</div>
);
......
......@@ -6,119 +6,158 @@
display: flex;
}
}
.time-picker-popover-popper {
z-index: $zindex-timepicker-popover;
}
.time-picker-utc {
color: $orange;
font-size: 75%;
padding: 3px;
font-weight: 500;
margin-left: 4px;
position: relative;
}
.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;
background: $popover-bg;
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;
}
box-shadow: $popover-shadow;
position: absolute;
flex-direction: column;
max-width: 600px;
top: 48px;
right: 20px;
.time-picker-popover-body {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
padding: $space-md;
padding-bottom: 0;
}
.time-picker-popover-box-title {
font-size: $font-size-lg;
.time-picker-popover-title {
font-size: $font-size-md;
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-body-custom-ranges:first-child {
margin-right: $space-md;
}
.time-picker-popover-box-body-custom-ranges-input {
.time-picker-popover-body-custom-ranges-input {
display: flex;
flex-flow: row nowrap;
align-items: center;
margin: $spacer 0;
margin-bottom: $space-sm;
.our-custom-wrapper-class {
margin-left: $spacer;
width: 100%;
.time-picker-input-error {
box-shadow: inset 0 0px 5px $red;
}
.time-picker-input-error {
box-shadow: inset 0 0px 5px $red;
}
}
.time-picker-popover-box-footer {
.time-picker-popover-footer {
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
margin-top: $spacer;
justify-content: center;
padding: $space-md;
}
}
.time-picker-popover-header {
background: $popover-header-bg;
padding: $space-sm;
}
.time-picker-input {
max-width: 170px;
}
.react-calendar__navigation__label {
line-height: 31px;
padding-bottom: 0;
}
.react-calendar__navigation__arrow {
font-size: $font-size-lg;
}
$arrowPaddingToBorder: 7px;
$arrowPadding: $arrowPaddingToBorder * 3;
.react-calendar__navigation__next-button {
padding-left: $arrowPadding;
padding-right: $arrowPaddingToBorder;
}
.react-calendar__navigation__prev-button {
padding-left: $arrowPaddingToBorder;
padding-right: $arrowPadding;
}
.react-calendar__month-view__days__day--neighboringMonth abbr {
opacity: 0.35;
}
.react-calendar__month-view__days {
padding: 4px;
}
.time-picker-calendar {
border: 1px solid $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;
background-color: $input-label-bg;
border: 0;
}
.react-calendar__month-view__weekdays {
background-color: $popover-border-color;
background-color: $input-bg;
text-align: center;
abbr {
border: 0;
text-decoration: none;
cursor: default;
color: $popover-color;
color: $orange;
font-weight: $font-weight-semi-bold;
display: block;
padding: 4px 0 0 0;
}
}
.time-picker-calendar-tile {
color: $input-color;
background-color: $input-bg;
border: 0;
line-height: 22px;
color: $text-color;
background-color: inherit;
line-height: 26px;
font-size: $font-size-md;
border: 1px solid transparent;
border-radius: $border-radius;
&:hover {
box-shadow: $panel-editor-viz-item-shadow-hover;
background: $panel-editor-viz-item-bg-hover;
border: $panel-editor-viz-item-border-hover;
color: $text-color-strong;
}
}
button.time-picker-calendar-tile:hover {
font-weight: $font-weight-semi-bold;
.react-calendar__month-view__days {
background-color: $calendar-bg-days;
}
.react-calendar__tile--now {
background-color: $calendar-bg-now;
}
.react-calendar__navigation__label,
......@@ -128,47 +167,46 @@
}
.react-calendar__tile--now {
color: $orange;
border-radius: $border-radius;
}
.react-calendar__tile--active {
color: $blue;
.react-calendar__tile--active,
.react-calendar__tile--active:hover {
color: $white;
font-weight: $font-weight-semi-bold;
background: linear-gradient(0deg, $blue-base, $blue-shade);
box-shadow: none;
border: 1px solid transparent;
}
}
@media only screen and (max-width: 1116px) {
.time-picker-popover-custom-range-label {
padding-right: $space-xs;
}
@include media-breakpoint-down(md) {
.time-picker-popover {
margin-left: $spacer;
display: flex;
flex-flow: column nowrap;
max-width: 400px;
.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-title {
font-size: $font-size-md;
}
.time-picker-popover-box:first-child {
border-right: none;
border-bottom: 1px ridge;
.time-picker-popover-body {
padding: $space-sm;
display: flex;
flex-flow: column nowrap;
}
.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-body-custom-ranges:first-child {
margin-right: 0;
margin-bottom: $space-sm;
}
.time-picker-popover-box-footer {
.time-picker-popover-footer {
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
......@@ -177,13 +215,6 @@
}
.time-picker-calendar {
max-width: 500px;
width: 100%;
}
}
@media only screen and (max-width: 746px) {
.time-picker-popover {
margin-top: 48px;
}
}
import { TimeOption, TimeRange, TIME_FORMAT } from '../../types/time';
import { TimeRange, TIME_FORMAT, RawTimeRange, TimeZone } from '../../types/time';
import { describeTimeRange } from '../../utils/rangeutil';
import * as dateMath from '../../utils/datemath';
import { dateTime, DateTime, toUtc } from '../../utils/moment_wrapper';
import { isDateTime, dateTime, DateTime, toUtc } from '../../utils/moment_wrapper';
export const mapTimeOptionToTimeRange = (
timeOption: TimeOption,
isTimezoneUtc: boolean,
timezone?: dateMath.Timezone
): TimeRange => {
const fromMoment = stringToDateTimeType(timeOption.from, isTimezoneUtc, false, timezone);
const toMoment = stringToDateTimeType(timeOption.to, isTimezoneUtc, true, timezone);
export const rawToTimeRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
const from = stringToDateTimeType(raw.from, false, timeZone);
const to = stringToDateTimeType(raw.to, true, timeZone);
return { from: fromMoment, to: toMoment, raw: { from: timeOption.from, to: timeOption.to } };
return { from, to, raw };
};
export const stringToDateTimeType = (
value: string,
isTimezoneUtc: boolean,
roundUp?: boolean,
timezone?: dateMath.Timezone
): DateTime => {
export const stringToDateTimeType = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => {
if (isDateTime(value)) {
return value;
}
if (value.indexOf('now') !== -1) {
if (!dateMath.isValid(value)) {
return dateTime();
}
const parsed = dateMath.parse(value, roundUp, timezone);
const parsed = dateMath.parse(value, roundUp, timeZone);
return parsed || dateTime();
}
if (isTimezoneUtc) {
if (timeZone === 'utc') {
return toUtc(value, TIME_FORMAT);
}
return dateTime(value, TIME_FORMAT);
};
export const mapTimeRangeToRangeString = (timeRange: TimeRange): string => {
return describeTimeRange(timeRange.raw);
export const mapTimeRangeToRangeString = (timeRange: RawTimeRange): string => {
return describeTimeRange(timeRange);
};
export const isValidTimeString = (text: string) => dateMath.isValid(text);
......@@ -26,9 +26,14 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
referenceElement: PopperJS.ReferenceObject;
wrapperClassName?: string;
renderArrow?: RenderPopperArrowFn;
eventsEnabled?: boolean;
}
class Popper extends PureComponent<Props> {
static defaultProps: Partial<Props> = {
eventsEnabled: true,
};
render() {
const {
content,
......@@ -39,6 +44,8 @@ class Popper extends PureComponent<Props> {
className,
wrapperClassName,
renderArrow,
referenceElement,
eventsEnabled,
} = this.props;
return (
......@@ -49,7 +56,8 @@ class Popper extends PureComponent<Props> {
<Portal>
<ReactPopper
placement={placement}
referenceElement={this.props.referenceElement}
referenceElement={referenceElement}
eventsEnabled={eventsEnabled}
// TODO: move modifiers config to popper controller
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
>
......
......@@ -33,6 +33,7 @@ export { UnitPicker } from './UnitPicker/UnitPicker';
export { StatsPicker } from './StatsPicker/StatsPicker';
export { Input, InputStatus } from './Input/Input';
export { RefreshPicker } from './RefreshPicker/RefreshPicker';
export { TimePicker } from './TimePicker/TimePicker';
export { List } from './List/List';
// Renderless
......
......@@ -39,6 +39,7 @@ $gray-2: ${theme.colors.gray2};
$gray-3: ${theme.colors.gray3};
$gray-4: ${theme.colors.gray4};
$gray-5: ${theme.colors.gray5};
$gray-6: ${theme.colors.gray6};
$gray-blue: ${theme.colors.grayBlue};
$input-black: #09090b;
......@@ -282,6 +283,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade);
$popover-bg: $dark-2;
$popover-color: $text-color;
$popover-border-color: $dark-9;
$popover-header-bg: $dark-9;
$popover-shadow: 0 0 20px black;
$popover-help-bg: $btn-secondary-bg;
......@@ -395,4 +397,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2;
$vertical-resize-handle-bg: $dark-10;
$vertical-resize-handle-dots: $gray-1;
$vertical-resize-handle-dots-hover: $gray-2;
// Calendar
$calendar-bg-days: $input-bg;
$calendar-bg-now: $dark-10;
`;
......@@ -27,6 +27,8 @@ $black: ${theme.colors.black};
$dark-1: ${theme.colors.dark1};
$dark-2: ${theme.colors.dark2};
$dark-4: ${theme.colors.dark4};
$dark-10: ${theme.colors.dark10};
$gray-1: ${theme.colors.gray1};
$gray-2: ${theme.colors.gray2};
$gray-3: ${theme.colors.gray3};
......@@ -269,6 +271,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade);
$popover-bg: $page-bg;
$popover-color: $text-color;
$popover-border-color: $gray-5;
$popover-header-bg: $gray-5;
$popover-shadow: 0 0 20px $white;
$popover-help-bg: $btn-secondary-bg;
......@@ -382,4 +385,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6;
$vertical-resize-handle-bg: $gray-4;
$vertical-resize-handle-dots: $gray-3;
$vertical-resize-handle-dots-hover: $gray-2;
// Calendar
$calendar-bg-days: $white;
$calendar-bg-now: $gray-6;
`;
......@@ -21,26 +21,17 @@ export interface IntervalValues {
intervalMs: number;
}
export interface TimeZone {
raw: string;
isUtc: boolean;
}
export const parseTimeZone = (raw: string): TimeZone => {
return {
raw,
isUtc: raw === 'utc',
};
};
export type TimeZoneUtc = 'utc';
export type TimeZoneBrowser = 'browser';
export type TimeZone = TimeZoneBrowser | TimeZoneUtc | string;
export const DefaultTimeZone = parseTimeZone('browser');
export const DefaultTimeZone: TimeZone = 'browser';
export interface TimeOption {
from: string;
to: string;
display: string;
section: number;
active: boolean;
}
export interface TimeOptions {
......
import includes from 'lodash/includes';
import isDate from 'lodash/isDate';
import { DateTime, dateTime, toUtc, ISO_8601, isDateTime, DurationUnit } from '../utils/moment_wrapper';
import { TimeZone } from '../types';
const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
export type Timezone = 'utc';
/**
* Parses different types input to a moment instance. There is a specific formatting language that can be used
* if text arg is string. See unit tests for examples.
......@@ -13,7 +12,7 @@ export type Timezone = 'utc';
* @param roundUp See parseDateMath function.
* @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used.
*/
export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: Timezone): DateTime | undefined {
export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: TimeZone): DateTime | undefined {
if (!text) {
return undefined;
}
......
......@@ -10,6 +10,7 @@ export * from './fieldDisplay';
export * from './deprecationWarning';
export * from './logs';
export * from './labels';
export * from './labels';
export { getMappedValue } from './valueMappings';
export * from './validate';
export { getFlotPairs } from './flotPairs';
......
......@@ -51,7 +51,6 @@ const rangeOptions = [
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 },
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 },
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 },
......@@ -62,7 +61,7 @@ const rangeOptions = [
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
];
const absoluteFormat = 'MMM D, YYYY HH:mm:ss';
const absoluteFormat = 'YYYY-MM-DD HH:mm:ss';
const rangeIndex: any = {};
_.each(rangeOptions, (frame: any) => {
......
......@@ -72,7 +72,7 @@ describe('rangeUtil', () => {
from: dateTime([2014, 10, 10, 2, 3, 4]),
to: 'now',
});
expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago');
expect(text).toBe('2014-11-10 02:03:04 to a few seconds ago');
});
it('Date range with absolute to relative', () => {
......@@ -80,7 +80,7 @@ describe('rangeUtil', () => {
from: dateTime([2014, 10, 10, 2, 3, 4]),
to: 'now-1d',
});
expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago');
expect(text).toBe('2014-11-10 02:03:04 to a day ago');
});
it('Date range with relative to absolute', () => {
......@@ -88,7 +88,7 @@ describe('rangeUtil', () => {
from: 'now-7d',
to: dateTime([2014, 10, 10, 2, 3, 4]),
});
expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04');
expect(text).toBe('7 days ago to 2014-11-10 02:03:04');
});
it('Date range with non matching default ranges', () => {
......
......@@ -366,8 +366,8 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourc
export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => {
return {
from: dateMath.parse(rawRange.from, false, timeZone.raw as any),
to: dateMath.parse(rawRange.to, true, timeZone.raw as any),
from: dateMath.parse(rawRange.from, false, timeZone as any),
to: dateMath.parse(rawRange.to, true, timeZone as any),
raw: rawRange,
};
};
......@@ -406,8 +406,8 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti
};
return {
from: dateMath.parse(raw.from, false, timeZone.raw as any),
to: dateMath.parse(raw.to, true, timeZone.raw as any),
from: dateMath.parse(raw.from, false, timeZone as any),
to: dateMath.parse(raw.to, true, timeZone as any),
raw,
};
};
......
......@@ -3,7 +3,6 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
// Utils & Services
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { appEvents } from 'app/core/app_events';
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
......@@ -36,8 +35,6 @@ export interface StateProps {
type Props = StateProps & OwnProps;
export class DashNav extends PureComponent<Props> {
timePickerEl: HTMLElement;
timepickerCmp: AngularComponent;
playlistSrv: PlaylistSrv;
constructor(props: Props) {
......@@ -45,21 +42,6 @@ export class DashNav extends PureComponent<Props> {
this.playlistSrv = this.props.$injector.get('playlistSrv');
}
componentDidMount() {
const loader = getAngularLoader();
const template =
'<gf-time-picker class="gf-timepicker-nav" dashboard="dashboard" ng-if="!dashboard.timepicker.hidden" />';
const scopeProps = { dashboard: this.props.dashboard };
this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template);
}
componentWillUnmount() {
if (this.timepickerCmp) {
this.timepickerCmp.destroy();
}
}
onDahboardNameClick = () => {
appEvents.emit('show-dash-search');
};
......@@ -187,7 +169,7 @@ export class DashNav extends PureComponent<Props> {
}
render() {
const { dashboard, onAddPanel, location } = this.props;
const { dashboard, onAddPanel, location, $injector } = this.props;
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl;
......@@ -281,8 +263,12 @@ export class DashNav extends PureComponent<Props> {
{!dashboard.timepicker.hidden && (
<div className="navbar-buttons">
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
<DashNavTimeControls
$injector={$injector}
dashboard={dashboard}
location={location}
updateLocation={updateLocation}
/>
</div>
)}
</div>
......
// Libaries
import React, { Component } from 'react';
import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
// Types
import { DashboardModel } from '../../state';
import { LocationState } from 'app/types';
import { TimeRange, TimeOption } from '@grafana/ui';
// State
import { updateLocation } from 'app/core/actions';
// Components
import { RefreshPicker } from '@grafana/ui';
import { TimePicker, RefreshPicker, RawTimeRange } from '@grafana/ui';
// Utils & Services
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
export interface Props {
$injector: any;
dashboard: DashboardModel;
updateLocation: typeof updateLocation;
location: LocationState;
......@@ -22,6 +26,7 @@ export interface Props {
export class DashNavTimeControls extends Component<Props> {
timeSrv: TimeSrv = getTimeSrv();
$rootScope = this.props.$injector.get('$rootScope');
get refreshParamInUrl(): string {
return this.props.location.query.refresh as string;
......@@ -37,17 +42,92 @@ export class DashNavTimeControls extends Component<Props> {
return Promise.resolve();
};
onMoveTimePicker = (direction: number) => {
const range = this.timeSrv.timeRange();
const timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
let to: number, from: number;
if (direction === -1) {
to = range.to.valueOf() - timespan;
from = range.from.valueOf() - timespan;
} else if (direction === 1) {
to = range.to.valueOf() + timespan;
from = range.from.valueOf() + timespan;
if (to > Date.now() && range.to.valueOf() < Date.now()) {
to = Date.now();
from = range.from.valueOf();
}
} else {
to = range.to.valueOf();
from = range.from.valueOf();
}
this.timeSrv.setTime({
from: toUtc(from),
to: toUtc(to),
});
};
onMoveForward = () => this.onMoveTimePicker(1);
onMoveBack = () => this.onMoveTimePicker(-1);
onChangeTimePicker = (timeRange: TimeRange) => {
const { dashboard } = this.props;
const panel = dashboard.timepicker;
const hasDelay = panel.nowDelay && timeRange.raw.to === 'now';
const nextRange = {
from: timeRange.raw.from,
to: hasDelay ? 'now-' + panel.nowDelay : timeRange.raw.to,
};
this.timeSrv.setTime(nextRange);
};
onZoom = () => {
this.$rootScope.appEvent('zoom-out', 2);
};
setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => {
return timeOptions.map(option => {
if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) {
return {
...option,
active: true,
};
}
return {
...option,
active: false,
};
});
};
render() {
const { dashboard } = this.props;
const intervals = dashboard.timepicker.refresh_intervals;
const timePickerValue = this.timeSrv.timeRange();
const timeZone = dashboard.getTimezone();
return (
<RefreshPicker
onIntervalChanged={this.onChangeRefreshInterval}
onRefresh={this.onRefresh}
value={dashboard.refresh}
intervals={intervals}
tooltip="Refresh dashboard"
/>
<>
<TimePicker
value={timePickerValue}
onChange={this.onChangeTimePicker}
timeZone={timeZone}
onMoveBackward={this.onMoveBack}
onMoveForward={this.onMoveForward}
onZoom={this.onZoom}
selectOptions={this.setActiveTimeOption(defaultSelectOptions, timePickerValue.raw)}
/>
<RefreshPicker
onIntervalChanged={this.onChangeRefreshInterval}
onRefresh={this.onRefresh}
value={dashboard.refresh}
intervals={intervals}
tooltip="Refresh dashboard"
/>
</>
);
}
}
......@@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module';
import * as dateMath from '@grafana/ui/src/utils/datemath';
// Types
import { TimeRange, RawTimeRange } from '@grafana/ui';
import { TimeRange, RawTimeRange, TimeZone } from '@grafana/ui';
import { ITimeoutService, ILocationService } from 'angular';
import { ContextSrv } from 'app/core/services/context_srv';
import { DashboardModel } from '../state/DashboardModel';
......@@ -224,7 +224,7 @@ export class TimeSrv {
to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to,
};
const timezone = this.dashboard && this.dashboard.getTimezone();
const timezone: TimeZone = this.dashboard ? this.dashboard.getTimezone() : undefined;
return {
from: dateMath.parse(raw.from, false, timezone),
......
......@@ -13,7 +13,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys';
// Types
import { PanelModel, GridPos } from './PanelModel';
import { DashboardMigrator } from './DashboardMigrator';
import { TimeRange } from '@grafana/ui';
import { TimeRange, TimeZone } from '@grafana/ui';
import { UrlQueryValue } from '@grafana/runtime';
import { KIOSK_MODE_TV, DashboardMeta } from 'app/types';
import { toUtc, DateTimeInput, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
......@@ -832,8 +832,8 @@ export class DashboardModel {
return this.snapshot !== undefined;
}
getTimezone() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
getTimezone(): TimeZone {
return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone;
}
private updateSchema(old: any) {
......
......@@ -216,7 +216,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
<div className="explore-toolbar-content-item timepicker">
{!isLive && (
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
<TimePicker ref={timepickerRef} range={range} isUtc={timeZone.isUtc} onChangeTime={onChangeTime} />
<TimePicker ref={timepickerRef} range={range} timeZone={timeZone} onChangeTime={onChangeTime} />
</ClickOutsideWrapper>
)}
......
......@@ -129,7 +129,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
this.$el.unbind('plotselected', this.onPlotSelected);
}
onPlotSelected = (event, ranges) => {
onPlotSelected = (event: JQueryEventObject, ranges) => {
const { onChangeTime } = this.props;
if (onChangeTime) {
this.props.onChangeTime({
......@@ -151,7 +151,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
max: max,
label: 'Datetime',
ticks: ticks,
timezone: timeZone.raw,
timezone: timeZone,
timeformat: time_format(ticks, min, max),
},
};
......
......@@ -34,8 +34,8 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
onChangeTime = (absRange: AbsoluteTimeRange) => {
const { exploreId, timeZone, changeTime } = this.props;
const range = {
from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to),
from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to),
};
changeTime(exploreId, range);
......
......@@ -57,8 +57,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onChangeTime = (absRange: AbsoluteTimeRange) => {
const { exploreId, timeZone, changeTime } = this.props;
const range = {
from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to),
from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to),
};
changeTime(exploreId, range);
......
......@@ -29,7 +29,7 @@ const fromRaw = (rawRange: RawTimeRange): TimeRange => {
describe('<TimePicker />', () => {
it('render default values when closed and relative time range', () => {
const range = fromRaw(DEFAULT_RANGE);
const wrapper = shallow(<TimePicker range={range} />);
const wrapper = shallow(<TimePicker range={range} timeZone="browser" />);
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
......@@ -39,7 +39,7 @@ describe('<TimePicker />', () => {
it('render default values when closed, utc and relative time range', () => {
const range = fromRaw(DEFAULT_RANGE);
const wrapper = shallow(<TimePicker range={range} isUtc />);
const wrapper = shallow(<TimePicker range={range} timeZone="utc" />);
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
......@@ -49,7 +49,7 @@ describe('<TimePicker />', () => {
it('renders default values when open and relative range', () => {
const range = fromRaw(DEFAULT_RANGE);
const wrapper = shallow(<TimePicker range={range} isOpen />);
const wrapper = shallow(<TimePicker range={range} isOpen timeZone="browser" />);
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
......@@ -61,7 +61,7 @@ describe('<TimePicker />', () => {
it('renders default values when open, utc and relative range', () => {
const range = fromRaw(DEFAULT_RANGE);
const wrapper = shallow(<TimePicker range={range} isOpen isUtc />);
const wrapper = shallow(<TimePicker range={range} isOpen timeZone="utc" />);
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
......@@ -91,7 +91,7 @@ describe('<TimePicker />', () => {
const expectedRangeString = rangeUtil.describeTimeRange(localRange);
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
expect(wrapper.state('initialRange')).toBe(range.raw);
......@@ -118,11 +118,11 @@ describe('<TimePicker />', () => {
},
};
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} timeZone="utc" isOpen onChangeTime={onChangeTime} />);
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00');
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01');
expect(wrapper.state('initialRange')).toBe(range.raw);
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01');
expect(wrapper.find('.timepicker-rangestring').text()).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01');
expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:00');
expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01');
......@@ -132,7 +132,7 @@ describe('<TimePicker />', () => {
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000);
expect(wrapper.state('isOpen')).toBeFalsy();
expect(wrapper.state('rangeString')).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01');
expect(wrapper.state('rangeString')).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01');
});
it('moves ranges backward by half the range on left arrow click when utc', () => {
......@@ -147,7 +147,7 @@ describe('<TimePicker />', () => {
const range = fromRaw(rawRange);
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
......@@ -176,7 +176,7 @@ describe('<TimePicker />', () => {
};
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isUtc={false} isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
......@@ -197,7 +197,7 @@ describe('<TimePicker />', () => {
};
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01');
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03');
......@@ -226,7 +226,7 @@ describe('<TimePicker />', () => {
};
const onChangeTime = sinon.spy();
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
......
import React, { PureComponent } from 'react';
import React, { PureComponent, ChangeEvent } from 'react';
import * as rangeUtil from '@grafana/ui/src/utils/rangeutil';
import { Input, RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui';
import { Input, RawTimeRange, TimeRange, TIME_FORMAT, TimeZone } from '@grafana/ui';
import { toUtc, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper';
interface TimePickerProps {
isOpen?: boolean;
isUtc?: boolean;
range: TimeRange;
timeZone: TimeZone;
onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void;
}
......@@ -22,21 +22,21 @@ interface TimePickerState {
toRaw: string;
}
const getRaw = (isUtc: boolean, range: any) => {
const getRaw = (range: any, timeZone: TimeZone) => {
const rawRange = {
from: range.raw.from,
to: range.raw.to,
};
if (isDateTime(rawRange.from)) {
if (!isUtc) {
if (timeZone === 'browser') {
rawRange.from = rawRange.from.local();
}
rawRange.from = rawRange.from.format(TIME_FORMAT);
}
if (isDateTime(rawRange.to)) {
if (!isUtc) {
if (timeZone === 'browser') {
rawRange.to = rawRange.to.local();
}
rawRange.to = rawRange.to.format(TIME_FORMAT);
......@@ -61,19 +61,19 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
constructor(props) {
super(props);
const { range, isUtc, isOpen } = props;
const rawRange = getRaw(props.isUtc, range);
const { range, timeZone, isOpen } = props;
const rawRange = getRaw(range, timeZone);
this.state = {
isOpen: isOpen,
isUtc: isUtc,
isUtc: timeZone === 'utc',
rangeString: rangeUtil.describeTimeRange(range.raw),
fromRaw: rawRange.from,
toRaw: rawRange.to,
initialRange: range.raw,
refreshInterval: '',
};
} //Temp solution... How do detect if ds supports table format?
}
static getDerivedStateFromProps(props: TimePickerProps, state: TimePickerState) {
if (
......@@ -85,7 +85,7 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
}
const { range } = props;
const rawRange = getRaw(props.isUtc, range);
const rawRange = getRaw(range, props.timeZone);
return {
...state,
......@@ -102,8 +102,10 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
from: toUtc(origRange.from),
to: toUtc(origRange.to),
};
const timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
let to, from;
if (direction === -1) {
to = range.to.valueOf() - timespan;
from = range.from.valueOf() - timespan;
......@@ -116,30 +118,32 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
}
const nextTimeRange = {
from: this.props.isUtc ? toUtc(from) : dateTime(from),
to: this.props.isUtc ? toUtc(to) : dateTime(to),
from: this.props.timeZone === 'utc' ? toUtc(from) : dateTime(from),
to: this.props.timeZone === 'utc' ? toUtc(to) : dateTime(to),
};
if (onChangeTime) {
onChangeTime(nextTimeRange);
}
return nextTimeRange;
}
handleChangeFrom = e => {
handleChangeFrom = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
fromRaw: e.target.value,
fromRaw: event.target.value,
});
};
handleChangeTo = e => {
handleChangeTo = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
toRaw: e.target.value,
toRaw: event.target.value,
});
};
handleClickApply = () => {
const { onChangeTime, isUtc } = this.props;
const { onChangeTime, timeZone } = this.props;
let rawRange;
this.setState(
state => {
const { toRaw, fromRaw } = this.state;
......@@ -149,11 +153,11 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
};
if (rawRange.from.indexOf('now') === -1) {
rawRange.from = isUtc ? toUtc(rawRange.from, TIME_FORMAT) : dateTime(rawRange.from, TIME_FORMAT);
rawRange.from = timeZone === 'utc' ? toUtc(rawRange.from, TIME_FORMAT) : dateTime(rawRange.from, TIME_FORMAT);
}
if (rawRange.to.indexOf('now') === -1) {
rawRange.to = isUtc ? toUtc(rawRange.to, TIME_FORMAT) : dateTime(rawRange.to, TIME_FORMAT);
rawRange.to = timeZone === 'utc' ? toUtc(rawRange.to, TIME_FORMAT) : dateTime(rawRange.to, TIME_FORMAT);
}
const rangeString = rangeUtil.describeTimeRange(rawRange);
......
import { UserState } from 'app/types';
import { parseTimeZone } from '@grafana/ui';
export const getTimeZone = (state: UserState) => parseTimeZone(state.timeZone);
export const getTimeZone = (state: UserState) => state.timeZone;
......@@ -115,6 +115,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "Is team member",
......@@ -199,6 +200,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "Is team member",
......
......@@ -6,14 +6,14 @@ describe('grafana data source', () => {
describe('when executing an annotations query', () => {
let calledBackendSrvParams;
const backendSrvStub = {
get: (url, options) => {
get: (url: string, options) => {
calledBackendSrvParams = options;
return q.resolve([]);
},
};
const templateSrvStub = {
replace: val => {
replace: (val: string) => {
return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced');
},
};
......
......@@ -123,6 +123,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "1/1",
......@@ -176,6 +177,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Time series",
......
......@@ -149,7 +149,7 @@ class GraphElement {
}
}
onPlotSelected(event, ranges) {
onPlotSelected(event: JQueryEventObject, ranges) {
if (this.panel.xaxis.mode !== 'time') {
// Skip if panel in histogram or series mode
this.plot.clearSelection();
......@@ -171,7 +171,7 @@ class GraphElement {
}
}
onPlotClick(event, pos, item) {
onPlotClick(event: JQueryEventObject, pos, item) {
if (this.panel.xaxis.mode !== 'time') {
// Skip if panel in histogram or series mode
return;
......
import { TimeZone } from '@grafana/ui/src/types';
export interface OrgUser {
avatarUrl: string;
email: string;
......@@ -46,7 +48,7 @@ export interface UsersState {
export interface UserState {
orgId: number;
timeZone: string;
timeZone: TimeZone;
}
export interface UserSession {
......
......@@ -42,6 +42,7 @@ $gray-2: #8e8e8e;
$gray-3: #b3b3b3;
$gray-4: #d8d9da;
$gray-5: #ececec;
$gray-6: #f4f5f8;
$gray-blue: #212327;
$input-black: #09090b;
......@@ -285,6 +286,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade);
$popover-bg: $dark-2;
$popover-color: $text-color;
$popover-border-color: $dark-9;
$popover-header-bg: $dark-9;
$popover-shadow: 0 0 20px black;
$popover-help-bg: $btn-secondary-bg;
......@@ -398,3 +400,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2;
$vertical-resize-handle-bg: $dark-10;
$vertical-resize-handle-dots: $gray-1;
$vertical-resize-handle-dots-hover: $gray-2;
// Calendar
$calendar-bg-days: $input-bg;
$calendar-bg-now: $dark-10;
......@@ -30,6 +30,8 @@ $black: #000000;
$dark-1: #1e2028;
$dark-2: #41444b;
$dark-4: #35373f;
$dark-10: #424345;
$gray-1: #52545c;
$gray-2: #767980;
$gray-3: #acb6bf;
......@@ -272,6 +274,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade);
$popover-bg: $page-bg;
$popover-color: $text-color;
$popover-border-color: $gray-5;
$popover-header-bg: $gray-5;
$popover-shadow: 0 0 20px $white;
$popover-help-bg: $btn-secondary-bg;
......@@ -385,3 +388,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6;
$vertical-resize-handle-bg: $gray-4;
$vertical-resize-handle-dots: $gray-3;
$vertical-resize-handle-dots-hover: $gray-2;
// Calendar
$calendar-bg-days: $white;
$calendar-bg-now: $gray-6;
......@@ -134,7 +134,7 @@ i.navbar-page-btn__search {
align-items: center;
font-weight: $btn-font-weight;
padding: 6px $space-sm;
line-height: 16px;
line-height: 18px;
color: $text-muted;
border: 1px solid $navbar-button-border;
margin-left: $space-xs;
......
......@@ -304,7 +304,7 @@
}
.graph-annotation__header {
background-color: $popover-border-color;
background: $popover-header-bg;
padding: 6px 10px;
display: flex;
}
......
......@@ -252,7 +252,7 @@ $column-horizontal-spacing: 10px;
}
.logs-stats__header {
background-color: $popover-border-color;
background: $popover-header-bg;
padding: 6px 10px;
display: flex;
}
......
......@@ -19,7 +19,7 @@ export function ControllerTestContext(this: any) {
getMetricSources: () => {},
get: () => {
return {
then: (callback: (a: any) => void) => {
then: (callback: (ds: any) => void) => {
callback(self.datasource);
},
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment