mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
0adbb001db
commit
0412a28d2e
@ -1,9 +1,9 @@
|
||||
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);
|
||||
|
@ -1,275 +1,142 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
// Libraries
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
|
||||
// Components
|
||||
import { ButtonSelect } from '../Select/ButtonSelect';
|
||||
import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time';
|
||||
import { Props as TimePickerPopoverProps } from './TimePickerPopover';
|
||||
import { TimePickerOptionGroup } from './TimePickerOptionGroup';
|
||||
import { PopperContent } from '../Tooltip/PopperController';
|
||||
import { Timezone } from '../../utils/datemath';
|
||||
import { TimeRange, TimeOption, TimeOptions } from '../../types/time';
|
||||
import { SelectOptionItem } from '../Select/Select';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { TimePickerPopover } from './TimePickerPopover';
|
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
|
||||
// Utils & Services
|
||||
import { isDateTime } from '../../utils/moment_wrapper';
|
||||
import * as rangeUtil from '../../utils/rangeutil';
|
||||
import { rawToTimeRange } from './time';
|
||||
|
||||
// Types
|
||||
import { TimeRange, TimeOption, TimeZone, TIME_FORMAT } from '../../types/time';
|
||||
import { SelectOptionItem } from '../Select/Select';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
isTimezoneUtc: boolean;
|
||||
popoverOptions: TimeOptions;
|
||||
selectOptions: TimeOption[];
|
||||
timezone?: Timezone;
|
||||
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 },
|
||||
export const defaultSelectOptions: TimeOption[] = [
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
{ 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 },
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 3 },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 3 },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 },
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 },
|
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 },
|
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 },
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 3 },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 3 },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 3 },
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today', section: 3 },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far', section: 3 },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week', section: 3 },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far', section: 3 },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month', section: 3 },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far', section: 3 },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year', section: 3 },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 3 },
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
const defaultZoomOutTooltip = () => {
|
||||
return (
|
||||
<>
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface State {
|
||||
isMenuOpen: boolean;
|
||||
isCustomOpen: boolean;
|
||||
}
|
||||
|
||||
export class TimePicker extends PureComponent<Props, State> {
|
||||
static defaultSelectOptions = defaultSelectOptions;
|
||||
static defaultPopoverOptions = defaultPopoverOptions;
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
|
||||
state: State = {
|
||||
isMenuOpen: false,
|
||||
isCustomOpen: false,
|
||||
};
|
||||
|
||||
mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => {
|
||||
const { value, popoverOptions, isTimezoneUtc, timezone } = this.props;
|
||||
const options = selectOptions.map(timeOption => {
|
||||
return { label: timeOption.display, value: timeOption };
|
||||
return {
|
||||
label: timeOption.display,
|
||||
value: timeOption,
|
||||
};
|
||||
});
|
||||
|
||||
const popoverProps: TimePickerPopoverProps = {
|
||||
value,
|
||||
options: popoverOptions,
|
||||
isTimezoneUtc,
|
||||
timezone,
|
||||
};
|
||||
options.unshift({
|
||||
label: 'Custom time range',
|
||||
value: { from: 'custom', to: 'custom', display: 'Custom', section: 1 },
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Custom',
|
||||
expanded: true,
|
||||
options,
|
||||
onPopoverOpen: () => undefined,
|
||||
onPopoverClose: (timeRange: TimeRange) => this.onPopoverClose(timeRange),
|
||||
popoverProps,
|
||||
},
|
||||
];
|
||||
return options;
|
||||
};
|
||||
|
||||
onSelectChanged = (item: SelectOptionItem<TimeOption>) => {
|
||||
const { isTimezoneUtc, onChange, timezone } = this.props;
|
||||
const { onChange, timeZone } = this.props;
|
||||
|
||||
// @ts-ignore
|
||||
onChange(mapTimeOptionToTimeRange(item.value, isTimezoneUtc, timezone));
|
||||
if (item.value && item.value.from === 'custom') {
|
||||
// this is to prevent the ClickOutsideWrapper from directly closing the popover
|
||||
setTimeout(() => {
|
||||
this.setState({ isCustomOpen: true });
|
||||
}, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.value) {
|
||||
onChange(rawToTimeRange({ from: item.value.from, to: item.value.to }, timeZone));
|
||||
}
|
||||
};
|
||||
|
||||
onChangeMenuOpenState = (isOpen: boolean) => {
|
||||
this.setState({
|
||||
isMenuOpen: isOpen,
|
||||
});
|
||||
};
|
||||
onOpenMenu = () => this.onChangeMenuOpenState(true);
|
||||
onCloseMenu = () => this.onChangeMenuOpenState(false);
|
||||
|
||||
onPopoverClose = (timeRange: TimeRange) => {
|
||||
onCustomChange = (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();
|
||||
this.setState({ isCustomOpen: false });
|
||||
};
|
||||
|
||||
onCloseCustom = () => {
|
||||
this.setState({ isCustomOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectOptions: selectTimeOptions,
|
||||
value,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onZoom,
|
||||
tooltipContent,
|
||||
} = this.props;
|
||||
const { selectOptions: selectTimeOptions, value, onMoveBackward, onMoveForward, onZoom, timeZone } = this.props;
|
||||
const { isCustomOpen } = this.state;
|
||||
const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions);
|
||||
const rangeString = mapTimeRangeToRangeString(value);
|
||||
const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value));
|
||||
const rangeString = rangeUtil.describeTimeRange(value.raw);
|
||||
|
||||
const label = (
|
||||
<>
|
||||
{isCustomOpen && <span>Custom time range</span>}
|
||||
{!isCustomOpen && <span>{rangeString}</span>}
|
||||
{timeZone === 'utc' && <span className="time-picker-utc">UTC</span>}
|
||||
</>
|
||||
);
|
||||
const isAbsolute = isDateTime(value.raw.to);
|
||||
|
||||
return (
|
||||
<div className="time-picker">
|
||||
<div className="time-picker" ref={this.pickerTriggerRef}>
|
||||
<div className="time-picker-buttons">
|
||||
{isAbsolute && (
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
|
||||
@ -278,27 +145,46 @@ export class TimePicker extends PureComponent<Props, State> {
|
||||
)}
|
||||
<ButtonSelect
|
||||
className="time-picker-button-select"
|
||||
value={value}
|
||||
label={rangeString}
|
||||
value={currentOption}
|
||||
label={label}
|
||||
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}
|
||||
tooltipContent={<TimePickerTooltipContent timeRange={value} />}
|
||||
/>
|
||||
{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>
|
||||
|
||||
<Tooltip content={defaultZoomOutTooltip} placement="bottom">
|
||||
<button className="btn navbar-button navbar-button--zoom" onClick={onZoom}>
|
||||
<i className="fa fa-search-minus" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{isCustomOpen && (
|
||||
<ClickOutsideWrapper onClick={this.onCloseCustom}>
|
||||
<TimePickerPopover value={value} timeZone={timeZone} onChange={this.onCustomChange} />
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => (
|
||||
<>
|
||||
{timeRange.from.format(TIME_FORMAT)}
|
||||
<br />
|
||||
to
|
||||
<br />
|
||||
{timeRange.to.format(TIME_FORMAT)}
|
||||
</>
|
||||
);
|
||||
|
||||
function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean {
|
||||
return range.raw.from === option.from && range.raw.to === option.to;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ TimePickerCalendarStories.add('default', () => (
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimePickerCalendar
|
||||
isTimezoneUtc={false}
|
||||
timeZone="browser"
|
||||
value={value}
|
||||
onChange={timeRange => {
|
||||
action('onChange fired')(timeRange);
|
||||
|
@ -1,45 +1,51 @@
|
||||
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" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,27 +1,27 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
));
|
@ -1,66 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -1,167 +1,118 @@
|
||||
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 className="time-picker-popover-body">
|
||||
<div className="time-picker-popover-body-custom-ranges">
|
||||
<div className="time-picker-popover-body-custom-ranges-input">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">From</label>
|
||||
<TimePickerInput
|
||||
roundup={false}
|
||||
timeZone={timeZone}
|
||||
value={from}
|
||||
onChange={this.onFromInputChanged}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar
|
||||
timeZone={timeZone}
|
||||
roundup={false}
|
||||
value={from}
|
||||
onChange={this.onFromCalendarChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-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 className="time-picker-popover-body-custom-ranges">
|
||||
<div className="time-picker-popover-body-custom-ranges-input">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">To</label>
|
||||
<TimePickerInput
|
||||
roundup={true}
|
||||
timeZone={timeZone}
|
||||
value={to}
|
||||
onChange={this.onToInputChanged}
|
||||
tabIndex={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-body-custom-ranges-calendar">
|
||||
<TimePickerCalendar roundup={true} timeZone={timeZone} value={to} onChange={this.onToCalendarChanged} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-popover-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 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;
|
||||
box-shadow: $popover-shadow;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
top: 48px;
|
||||
right: 20px;
|
||||
|
||||
.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-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-body-custom-ranges:first-child {
|
||||
margin-right: $space-md;
|
||||
}
|
||||
|
||||
.time-picker-popover-box-body-custom-ranges:first-child {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,38 @@
|
||||
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 type TimeZoneUtc = 'utc';
|
||||
export type TimeZoneBrowser = 'browser';
|
||||
export type TimeZone = TimeZoneBrowser | TimeZoneUtc | string;
|
||||
|
||||
export const parseTimeZone = (raw: string): TimeZone => {
|
||||
return {
|
||||
raw,
|
||||
isUtc: raw === 'utc',
|
||||
};
|
||||
};
|
||||
|
||||
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 {
|
||||
|
@ -1,11 +1,10 @@
|
||||
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>
|
||||
|
@ -1,20 +1,24 @@
|
||||
// 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));
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
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);
|
||||
|
@ -1,4 +1,3 @@
|
||||
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;
|
||||
|
@ -1,3 +1,5 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
818
public/vendor/flot/jquery.flot.pie.js
vendored
818
public/vendor/flot/jquery.flot.pie.js
vendored
@ -1,818 +0,0 @@
|
||||
/* Flot plugin for rendering pie charts.
|
||||
|
||||
Copyright (c) 2007-2013 IOLA and Ole Laursen.
|
||||
Licensed under the MIT license.
|
||||
|
||||
The plugin assumes that each series has a single data value, and that each
|
||||
value is a positive integer or zero. Negative numbers don't make sense for a
|
||||
pie chart, and have unpredictable results. The values do NOT need to be
|
||||
passed in as percentages; the plugin will calculate the total and per-slice
|
||||
percentages internally.
|
||||
|
||||
* Created by Brian Medendorp
|
||||
|
||||
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
|
||||
|
||||
The plugin supports these options:
|
||||
|
||||
series: {
|
||||
pie: {
|
||||
show: true/false
|
||||
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
|
||||
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
|
||||
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
|
||||
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
|
||||
offset: {
|
||||
top: integer value to move the pie up or down
|
||||
left: integer value to move the pie left or right, or 'auto'
|
||||
},
|
||||
stroke: {
|
||||
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
|
||||
width: integer pixel width of the stroke
|
||||
},
|
||||
label: {
|
||||
show: true/false, or 'auto'
|
||||
formatter: a user-defined function that modifies the text/style of the label text
|
||||
radius: 0-1 for percentage of fullsize, or a specified pixel length
|
||||
background: {
|
||||
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
|
||||
opacity: 0-1
|
||||
},
|
||||
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
|
||||
},
|
||||
combine: {
|
||||
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
|
||||
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
|
||||
label: any text value of what the combined slice should be labeled
|
||||
}
|
||||
highlight: {
|
||||
opacity: 0-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
More detail and specific examples can be found in the included HTML file.
|
||||
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
|
||||
// Maximum redraw attempts when fitting labels within the plot
|
||||
|
||||
var REDRAW_ATTEMPTS = 10;
|
||||
|
||||
// Factor by which to shrink the pie when fitting labels within the plot
|
||||
|
||||
var REDRAW_SHRINK = 0.95;
|
||||
|
||||
function init(plot) {
|
||||
|
||||
var canvas = null,
|
||||
target = null,
|
||||
maxRadius = null,
|
||||
centerLeft = null,
|
||||
centerTop = null,
|
||||
processed = false,
|
||||
options = null,
|
||||
ctx = null;
|
||||
|
||||
// interactive variables
|
||||
|
||||
var highlights = [];
|
||||
|
||||
// add hook to determine if pie plugin in enabled, and then perform necessary operations
|
||||
|
||||
plot.hooks.processOptions.push(function(plot, options) {
|
||||
if (options.series.pie.show) {
|
||||
|
||||
options.grid.show = false;
|
||||
|
||||
// set labels.show
|
||||
|
||||
if (options.series.pie.label.show == "auto") {
|
||||
if (options.legend.show) {
|
||||
options.series.pie.label.show = false;
|
||||
} else {
|
||||
options.series.pie.label.show = true;
|
||||
}
|
||||
}
|
||||
|
||||
// set radius
|
||||
|
||||
if (options.series.pie.radius == "auto") {
|
||||
if (options.series.pie.label.show) {
|
||||
options.series.pie.radius = 3/4;
|
||||
} else {
|
||||
options.series.pie.radius = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure sane tilt
|
||||
|
||||
if (options.series.pie.tilt > 1) {
|
||||
options.series.pie.tilt = 1;
|
||||
} else if (options.series.pie.tilt < 0) {
|
||||
options.series.pie.tilt = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||||
var options = plot.getOptions();
|
||||
if (options.series.pie.show) {
|
||||
if (options.grid.hoverable) {
|
||||
eventHolder.unbind("mousemove").mousemove(onMouseMove);
|
||||
}
|
||||
if (options.grid.clickable) {
|
||||
eventHolder.unbind("click").click(onClick);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
|
||||
var options = plot.getOptions();
|
||||
if (options.series.pie.show) {
|
||||
processDatapoints(plot, series, data, datapoints);
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.drawOverlay.push(function(plot, octx) {
|
||||
var options = plot.getOptions();
|
||||
if (options.series.pie.show) {
|
||||
drawOverlay(plot, octx);
|
||||
}
|
||||
});
|
||||
|
||||
plot.hooks.draw.push(function(plot, newCtx) {
|
||||
var options = plot.getOptions();
|
||||
if (options.series.pie.show) {
|
||||
draw(plot, newCtx);
|
||||
}
|
||||
});
|
||||
|
||||
function processDatapoints(plot, series, datapoints) {
|
||||
if (!processed) {
|
||||
processed = true;
|
||||
canvas = plot.getCanvas();
|
||||
target = $(canvas).parent();
|
||||
options = plot.getOptions();
|
||||
plot.setData(combine(plot.getData()));
|
||||
}
|
||||
}
|
||||
|
||||
function combine(data) {
|
||||
|
||||
var total = 0,
|
||||
combined = 0,
|
||||
numCombined = 0,
|
||||
color = options.series.pie.combine.color,
|
||||
newdata = [];
|
||||
|
||||
// Fix up the raw data from Flot, ensuring the data is numeric
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
|
||||
var value = data[i].data;
|
||||
|
||||
// If the data is an array, we'll assume that it's a standard
|
||||
// Flot x-y pair, and are concerned only with the second value.
|
||||
|
||||
// Note how we use the original array, rather than creating a
|
||||
// new one; this is more efficient and preserves any extra data
|
||||
// that the user may have stored in higher indexes.
|
||||
|
||||
if ($.isArray(value) && value.length == 1) {
|
||||
value = value[0];
|
||||
}
|
||||
|
||||
if ($.isArray(value)) {
|
||||
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
|
||||
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
|
||||
value[1] = +value[1];
|
||||
} else {
|
||||
value[1] = 0;
|
||||
}
|
||||
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
|
||||
value = [1, +value];
|
||||
} else {
|
||||
value = [1, 0];
|
||||
}
|
||||
|
||||
data[i].data = [value];
|
||||
}
|
||||
|
||||
// Sum up all the slices, so we can calculate percentages for each
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
total += data[i].data[0][1];
|
||||
}
|
||||
|
||||
// Count the number of slices with percentages below the combine
|
||||
// threshold; if it turns out to be just one, we won't combine.
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var value = data[i].data[0][1];
|
||||
if (value / total <= options.series.pie.combine.threshold) {
|
||||
combined += value;
|
||||
numCombined++;
|
||||
if (!color) {
|
||||
color = data[i].color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var value = data[i].data[0][1];
|
||||
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
|
||||
newdata.push({
|
||||
data: [[1, value]],
|
||||
color: data[i].color,
|
||||
label: data[i].label,
|
||||
angle: value * Math.PI * 2 / total,
|
||||
percent: value / (total / 100)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (numCombined > 1) {
|
||||
newdata.push({
|
||||
data: [[1, combined]],
|
||||
color: color,
|
||||
label: options.series.pie.combine.label,
|
||||
angle: combined * Math.PI * 2 / total,
|
||||
percent: combined / (total / 100)
|
||||
});
|
||||
}
|
||||
|
||||
return newdata;
|
||||
}
|
||||
|
||||
function draw(plot, newCtx) {
|
||||
|
||||
if (!target) {
|
||||
return; // if no series were passed
|
||||
}
|
||||
|
||||
var canvasWidth = plot.getPlaceholder().width(),
|
||||
canvasHeight = plot.getPlaceholder().height(),
|
||||
legendWidth = target.children().filter(".legend").children().width() || 0;
|
||||
|
||||
ctx = newCtx;
|
||||
|
||||
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
|
||||
|
||||
// When combining smaller slices into an 'other' slice, we need to
|
||||
// add a new series. Since Flot gives plugins no way to modify the
|
||||
// list of series, the pie plugin uses a hack where the first call
|
||||
// to processDatapoints results in a call to setData with the new
|
||||
// list of series, then subsequent processDatapoints do nothing.
|
||||
|
||||
// The plugin-global 'processed' flag is used to control this hack;
|
||||
// it starts out false, and is set to true after the first call to
|
||||
// processDatapoints.
|
||||
|
||||
// Unfortunately this turns future setData calls into no-ops; they
|
||||
// call processDatapoints, the flag is true, and nothing happens.
|
||||
|
||||
// To fix this we'll set the flag back to false here in draw, when
|
||||
// all series have been processed, so the next sequence of calls to
|
||||
// processDatapoints once again starts out with a slice-combine.
|
||||
// This is really a hack; in 0.9 we need to give plugins a proper
|
||||
// way to modify series before any processing begins.
|
||||
|
||||
processed = false;
|
||||
|
||||
// calculate maximum radius and center point
|
||||
|
||||
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
|
||||
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
|
||||
centerLeft = canvasWidth / 2;
|
||||
|
||||
if (options.series.pie.offset.left == "auto") {
|
||||
if (options.legend.position.match("w")) {
|
||||
centerLeft += legendWidth / 2;
|
||||
} else {
|
||||
centerLeft -= legendWidth / 2;
|
||||
}
|
||||
} else {
|
||||
centerLeft += options.series.pie.offset.left;
|
||||
}
|
||||
|
||||
if (centerLeft < maxRadius) {
|
||||
centerLeft = maxRadius;
|
||||
} else if (centerLeft > canvasWidth - maxRadius) {
|
||||
centerLeft = canvasWidth - maxRadius;
|
||||
}
|
||||
|
||||
var slices = plot.getData(),
|
||||
attempts = 0;
|
||||
|
||||
// Keep shrinking the pie's radius until drawPie returns true,
|
||||
// indicating that all the labels fit, or we try too many times.
|
||||
|
||||
do {
|
||||
if (attempts > 0) {
|
||||
maxRadius *= REDRAW_SHRINK;
|
||||
}
|
||||
attempts += 1;
|
||||
clear();
|
||||
if (options.series.pie.tilt <= 0.8) {
|
||||
drawShadow();
|
||||
}
|
||||
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
|
||||
|
||||
if (attempts >= REDRAW_ATTEMPTS) {
|
||||
clear();
|
||||
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
|
||||
}
|
||||
|
||||
if (plot.setSeries && plot.insertLegend) {
|
||||
plot.setSeries(slices);
|
||||
plot.insertLegend();
|
||||
}
|
||||
|
||||
// we're actually done at this point, just defining internal functions at this point
|
||||
|
||||
function clear() {
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
target.children().filter(".pieLabel, .pieLabelBackground").remove();
|
||||
}
|
||||
|
||||
function drawShadow() {
|
||||
|
||||
var shadowLeft = options.series.pie.shadow.left;
|
||||
var shadowTop = options.series.pie.shadow.top;
|
||||
var edge = 10;
|
||||
var alpha = options.series.pie.shadow.alpha;
|
||||
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||
|
||||
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
|
||||
return; // shadow would be outside canvas, so don't draw it
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(shadowLeft,shadowTop);
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = "#000";
|
||||
|
||||
// center and rotate to starting position
|
||||
|
||||
ctx.translate(centerLeft,centerTop);
|
||||
ctx.scale(1, options.series.pie.tilt);
|
||||
|
||||
//radius -= edge;
|
||||
|
||||
for (var i = 1; i <= edge; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
|
||||
ctx.fill();
|
||||
radius -= i;
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPie() {
|
||||
|
||||
var startAngle = Math.PI * options.series.pie.startAngle;
|
||||
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||
|
||||
// center and rotate to starting position
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(centerLeft,centerTop);
|
||||
ctx.scale(1, options.series.pie.tilt);
|
||||
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
|
||||
|
||||
// draw slices
|
||||
|
||||
ctx.save();
|
||||
var currentAngle = startAngle;
|
||||
for (var i = 0; i < slices.length; ++i) {
|
||||
slices[i].startAngle = currentAngle;
|
||||
drawSlice(slices[i].angle, slices[i].color, true);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// draw slice outlines
|
||||
|
||||
if (options.series.pie.stroke.width > 0) {
|
||||
ctx.save();
|
||||
ctx.lineWidth = options.series.pie.stroke.width;
|
||||
currentAngle = startAngle;
|
||||
for (var i = 0; i < slices.length; ++i) {
|
||||
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// draw donut hole
|
||||
|
||||
drawDonutHole(ctx);
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Draw the labels, returning true if they fit within the plot
|
||||
|
||||
if (options.series.pie.label.show) {
|
||||
return drawLabels();
|
||||
} else return true;
|
||||
|
||||
function drawSlice(angle, color, fill) {
|
||||
|
||||
if (angle <= 0 || isNaN(angle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fill) {
|
||||
ctx.fillStyle = color;
|
||||
} else {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineJoin = "round";
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
|
||||
ctx.moveTo(0, 0); // Center of the pie
|
||||
}
|
||||
|
||||
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
|
||||
ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
|
||||
ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
|
||||
ctx.closePath();
|
||||
//ctx.rotate(angle); // This doesn't work properly in Opera
|
||||
currentAngle += angle;
|
||||
|
||||
if (fill) {
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLabels() {
|
||||
|
||||
var currentAngle = startAngle;
|
||||
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
|
||||
|
||||
for (var i = 0; i < slices.length; ++i) {
|
||||
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
|
||||
if (!drawLabel(slices[i], currentAngle, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
currentAngle += slices[i].angle;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
function drawLabel(slice, startAngle, index) {
|
||||
|
||||
if (slice.data[0][1] == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// format label text
|
||||
|
||||
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
|
||||
|
||||
if (lf) {
|
||||
text = lf(slice.label, slice);
|
||||
} else {
|
||||
text = slice.label;
|
||||
}
|
||||
|
||||
if (plf) {
|
||||
text = plf(text, slice);
|
||||
}
|
||||
|
||||
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
|
||||
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
|
||||
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
|
||||
|
||||
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
|
||||
target.append(html);
|
||||
|
||||
var label = target.children("#pieLabel" + index);
|
||||
var labelTop = (y - label.height() / 2);
|
||||
var labelLeft = (x - label.width() / 2);
|
||||
|
||||
label.css("top", labelTop);
|
||||
label.css("left", labelLeft);
|
||||
|
||||
// check to make sure that the label is not outside the canvas
|
||||
|
||||
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.series.pie.label.background.opacity != 0) {
|
||||
|
||||
// put in the transparent background separately to avoid blended labels and label boxes
|
||||
|
||||
var c = options.series.pie.label.background.color;
|
||||
|
||||
if (c == null) {
|
||||
c = slice.color;
|
||||
}
|
||||
|
||||
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
|
||||
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
|
||||
.css("opacity", options.series.pie.label.background.opacity)
|
||||
.insertBefore(label);
|
||||
}
|
||||
|
||||
return true;
|
||||
} // end individual label function
|
||||
} // end drawLabels function
|
||||
} // end drawPie function
|
||||
} // end draw function
|
||||
|
||||
// Placed here because it needs to be accessed from multiple locations
|
||||
|
||||
function drawDonutHole(layer) {
|
||||
if (options.series.pie.innerRadius > 0) {
|
||||
|
||||
// subtract the center
|
||||
|
||||
layer.save();
|
||||
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
|
||||
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
|
||||
layer.beginPath();
|
||||
layer.fillStyle = options.series.pie.stroke.color;
|
||||
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||||
layer.fill();
|
||||
layer.closePath();
|
||||
layer.restore();
|
||||
|
||||
// add inner stroke
|
||||
|
||||
layer.save();
|
||||
layer.beginPath();
|
||||
layer.strokeStyle = options.series.pie.stroke.color;
|
||||
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||||
layer.stroke();
|
||||
layer.closePath();
|
||||
layer.restore();
|
||||
|
||||
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
|
||||
}
|
||||
}
|
||||
|
||||
//-- Additional Interactive related functions --
|
||||
|
||||
function isPointInPoly(poly, pt) {
|
||||
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
|
||||
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
|
||||
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
|
||||
&& (c = !c);
|
||||
return c;
|
||||
}
|
||||
|
||||
function findNearbySlice(mouseX, mouseY) {
|
||||
|
||||
var slices = plot.getData(),
|
||||
options = plot.getOptions(),
|
||||
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
|
||||
x, y;
|
||||
|
||||
for (var i = 0; i < slices.length; ++i) {
|
||||
|
||||
var s = slices[i];
|
||||
|
||||
if (s.pie.show) {
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0); // Center of the pie
|
||||
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
|
||||
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
|
||||
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
|
||||
ctx.closePath();
|
||||
x = mouseX - centerLeft;
|
||||
y = mouseY - centerTop;
|
||||
|
||||
if (ctx.isPointInPath) {
|
||||
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
|
||||
ctx.restore();
|
||||
return {
|
||||
datapoint: [s.percent, s.data],
|
||||
dataIndex: 0,
|
||||
series: s,
|
||||
seriesIndex: i
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
||||
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
|
||||
|
||||
var p1X = radius * Math.cos(s.startAngle),
|
||||
p1Y = radius * Math.sin(s.startAngle),
|
||||
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
|
||||
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
|
||||
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
|
||||
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
|
||||
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
|
||||
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
|
||||
p5X = radius * Math.cos(s.startAngle + s.angle),
|
||||
p5Y = radius * Math.sin(s.startAngle + s.angle),
|
||||
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
|
||||
arrPoint = [x, y];
|
||||
|
||||
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
|
||||
|
||||
if (isPointInPoly(arrPoly, arrPoint)) {
|
||||
ctx.restore();
|
||||
return {
|
||||
datapoint: [s.percent, s.data],
|
||||
dataIndex: 0,
|
||||
series: s,
|
||||
seriesIndex: i
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
triggerClickHoverEvent("plothover", e);
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
triggerClickHoverEvent("plotclick", e);
|
||||
}
|
||||
|
||||
// trigger click or hover event (they send the same parameters so we share their code)
|
||||
|
||||
function triggerClickHoverEvent(eventname, e) {
|
||||
|
||||
var offset = plot.offset();
|
||||
var canvasX = parseInt(e.pageX - offset.left);
|
||||
var canvasY = parseInt(e.pageY - offset.top);
|
||||
var item = findNearbySlice(canvasX, canvasY);
|
||||
|
||||
if (options.grid.autoHighlight) {
|
||||
|
||||
// clear auto-highlights
|
||||
|
||||
for (var i = 0; i < highlights.length; ++i) {
|
||||
var h = highlights[i];
|
||||
if (h.auto == eventname && !(item && h.series == item.series)) {
|
||||
unhighlight(h.series);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// highlight the slice
|
||||
|
||||
if (item) {
|
||||
highlight(item.series, eventname);
|
||||
}
|
||||
|
||||
// trigger any hover bind events
|
||||
|
||||
var pos = { pageX: e.pageX, pageY: e.pageY };
|
||||
target.trigger(eventname, [pos, item]);
|
||||
}
|
||||
|
||||
function highlight(s, auto) {
|
||||
//if (typeof s == "number") {
|
||||
// s = series[s];
|
||||
//}
|
||||
|
||||
var i = indexOfHighlight(s);
|
||||
|
||||
if (i == -1) {
|
||||
highlights.push({ series: s, auto: auto });
|
||||
plot.triggerRedrawOverlay();
|
||||
} else if (!auto) {
|
||||
highlights[i].auto = false;
|
||||
}
|
||||
}
|
||||
|
||||
function unhighlight(s) {
|
||||
if (s == null) {
|
||||
highlights = [];
|
||||
plot.triggerRedrawOverlay();
|
||||
}
|
||||
|
||||
//if (typeof s == "number") {
|
||||
// s = series[s];
|
||||
//}
|
||||
|
||||
var i = indexOfHighlight(s);
|
||||
|
||||
if (i != -1) {
|
||||
highlights.splice(i, 1);
|
||||
plot.triggerRedrawOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function indexOfHighlight(s) {
|
||||
for (var i = 0; i < highlights.length; ++i) {
|
||||
var h = highlights[i];
|
||||
if (h.series == s)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function drawOverlay(plot, octx) {
|
||||
|
||||
var options = plot.getOptions();
|
||||
|
||||
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||
|
||||
octx.save();
|
||||
octx.translate(centerLeft, centerTop);
|
||||
octx.scale(1, options.series.pie.tilt);
|
||||
|
||||
for (var i = 0; i < highlights.length; ++i) {
|
||||
drawHighlight(highlights[i].series);
|
||||
}
|
||||
|
||||
drawDonutHole(octx);
|
||||
|
||||
octx.restore();
|
||||
|
||||
function drawHighlight(series) {
|
||||
|
||||
if (series.angle <= 0 || isNaN(series.angle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
|
||||
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
|
||||
octx.beginPath();
|
||||
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
|
||||
octx.moveTo(0, 0); // Center of the pie
|
||||
}
|
||||
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
|
||||
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
|
||||
octx.closePath();
|
||||
octx.fill();
|
||||
}
|
||||
}
|
||||
} // end init (plugin body)
|
||||
|
||||
// define pie specific options and their default values
|
||||
|
||||
var options = {
|
||||
series: {
|
||||
pie: {
|
||||
show: false,
|
||||
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
|
||||
innerRadius: 0, /* for donut */
|
||||
startAngle: 3/2,
|
||||
tilt: 1,
|
||||
shadow: {
|
||||
left: 5, // shadow left offset
|
||||
top: 15, // shadow top offset
|
||||
alpha: 0.02 // shadow alpha
|
||||
},
|
||||
offset: {
|
||||
top: 0,
|
||||
left: "auto"
|
||||
},
|
||||
stroke: {
|
||||
color: "#fff",
|
||||
width: 1
|
||||
},
|
||||
label: {
|
||||
show: "auto",
|
||||
formatter: function(label, slice) {
|
||||
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
|
||||
}, // formatter function
|
||||
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
|
||||
background: {
|
||||
color: null,
|
||||
opacity: 0
|
||||
},
|
||||
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
|
||||
},
|
||||
combine: {
|
||||
threshold: -1, // percentage at which to combine little slices into one larger slice
|
||||
color: null, // color to give the new slice (auto-generated if null)
|
||||
label: "Other" // label to give the new slice
|
||||
},
|
||||
highlight: {
|
||||
//color: "#fff", // will add this functionality once parseColor is available
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: "pie",
|
||||
version: "1.1"
|
||||
});
|
||||
|
||||
})(jQuery);
|
Loading…
Reference in New Issue
Block a user