+
-
- {Object.keys(options).map(key => {
- return (
-
- );
- })}
+
-
-
- Custom range
-
-
-
-
-
+
+
);
diff --git a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
index ffbbd009d7c..398cac9925c 100644
--- a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
+++ b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
@@ -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;
- }
-}
diff --git a/packages/grafana-ui/src/components/TimePicker/time.ts b/packages/grafana-ui/src/components/TimePicker/time.ts
index f697456eeb3..c794bf406dc 100644
--- a/packages/grafana-ui/src/components/TimePicker/time.ts
+++ b/packages/grafana-ui/src/components/TimePicker/time.ts
@@ -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);
diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx
index e439ecf0437..84b92d8233a 100644
--- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx
+++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx
@@ -26,9 +26,14 @@ interface Props extends React.HTMLAttributes
{
referenceElement: PopperJS.ReferenceObject;
wrapperClassName?: string;
renderArrow?: RenderPopperArrowFn;
+ eventsEnabled?: boolean;
}
class Popper extends PureComponent {
+ static defaultProps: Partial = {
+ eventsEnabled: true,
+ };
+
render() {
const {
content,
@@ -39,6 +44,8 @@ class Popper extends PureComponent {
className,
wrapperClassName,
renderArrow,
+ referenceElement,
+ eventsEnabled,
} = this.props;
return (
@@ -49,7 +56,8 @@ class Popper extends PureComponent {
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 860dd5a97ab..abc4d7c2c9f 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -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
diff --git a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
index eef3e6eedf7..c51de327841 100644
--- a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
+++ b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
@@ -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;
`;
diff --git a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
index 96b2936354c..b37e91ba3f4 100644
--- a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
+++ b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
@@ -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;
`;
diff --git a/packages/grafana-ui/src/types/time.ts b/packages/grafana-ui/src/types/time.ts
index c7120776fac..ae8137277f3 100644
--- a/packages/grafana-ui/src/types/time.ts
+++ b/packages/grafana-ui/src/types/time.ts
@@ -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 {
diff --git a/packages/grafana-ui/src/utils/datemath.ts b/packages/grafana-ui/src/utils/datemath.ts
index 5fde248313f..bd14403070b 100644
--- a/packages/grafana-ui/src/utils/datemath.ts
+++ b/packages/grafana-ui/src/utils/datemath.ts
@@ -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;
}
diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts
index 11eb7b72736..c757451dac2 100644
--- a/packages/grafana-ui/src/utils/index.ts
+++ b/packages/grafana-ui/src/utils/index.ts
@@ -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';
diff --git a/packages/grafana-ui/src/utils/rangeutil.ts b/packages/grafana-ui/src/utils/rangeutil.ts
index bcc27ecfd8e..01dc6a89cf8 100644
--- a/packages/grafana-ui/src/utils/rangeutil.ts
+++ b/packages/grafana-ui/src/utils/rangeutil.ts
@@ -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) => {
diff --git a/public/app/core/specs/rangeutil.test.ts b/public/app/core/specs/rangeutil.test.ts
index d23bbf9f0eb..3ecdb8b6c71 100644
--- a/public/app/core/specs/rangeutil.test.ts
+++ b/public/app/core/specs/rangeutil.test.ts
@@ -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', () => {
diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts
index 811950a9251..135490e6305 100644
--- a/public/app/core/utils/explore.ts
+++ b/public/app/core/utils/explore.ts
@@ -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,
};
};
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx
index 8db88e9ba55..89f28fe4041 100644
--- a/public/app/features/dashboard/components/DashNav/DashNav.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx
@@ -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 {
- timePickerEl: HTMLElement;
- timepickerCmp: AngularComponent;
playlistSrv: PlaylistSrv;
constructor(props: Props) {
@@ -45,21 +42,6 @@ export class DashNav extends PureComponent {
this.playlistSrv = this.props.$injector.get('playlistSrv');
}
- componentDidMount() {
- const loader = getAngularLoader();
- const template =
- '';
- 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 {
}
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 {
{!dashboard.timepicker.hidden && (
-
(this.timePickerEl = element)} />
-
+
)}
diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx
index 42d3a64fc4b..065596d73df 100644
--- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx
@@ -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 {
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 {
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 (
-
+ <>
+
+
+ >
);
}
}
diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts
index 9c415b9aea5..ca9b244b956 100644
--- a/public/app/features/dashboard/services/TimeSrv.ts
+++ b/public/app/features/dashboard/services/TimeSrv.ts
@@ -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),
diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts
index 88e92cd09df..ff8fe5ac8d6 100644
--- a/public/app/features/dashboard/state/DashboardModel.ts
+++ b/public/app/features/dashboard/state/DashboardModel.ts
@@ -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) {
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx
index 9d3cb984120..75f8cc75b7c 100644
--- a/public/app/features/explore/ExploreToolbar.tsx
+++ b/public/app/features/explore/ExploreToolbar.tsx
@@ -216,7 +216,7 @@ export class UnConnectedExploreToolbar extends PureComponent {
{!isLive && (
-
+
)}
diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx
index b5cdca318af..1a65bd2397c 100644
--- a/public/app/features/explore/Graph.tsx
+++ b/public/app/features/explore/Graph.tsx
@@ -129,7 +129,7 @@ export class Graph extends PureComponent
{
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 {
max: max,
label: 'Datetime',
ticks: ticks,
- timezone: timeZone.raw,
+ timezone: timeZone,
timeformat: time_format(ticks, min, max),
},
};
diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx
index 6d1bb6c4e38..632613f60c0 100644
--- a/public/app/features/explore/GraphContainer.tsx
+++ b/public/app/features/explore/GraphContainer.tsx
@@ -34,8 +34,8 @@ export class GraphContainer extends PureComponent {
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);
diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx
index c6ba84aceb0..975923015c5 100644
--- a/public/app/features/explore/LogsContainer.tsx
+++ b/public/app/features/explore/LogsContainer.tsx
@@ -57,8 +57,8 @@ export class LogsContainer extends PureComponent {
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);
diff --git a/public/app/features/explore/TimePicker.test.tsx b/public/app/features/explore/TimePicker.test.tsx
index 9cdf7a9b517..ea793096374 100644
--- a/public/app/features/explore/TimePicker.test.tsx
+++ b/public/app/features/explore/TimePicker.test.tsx
@@ -29,7 +29,7 @@ const fromRaw = (rawRange: RawTimeRange): TimeRange => {
describe('', () => {
it('render default values when closed and relative time range', () => {
const range = fromRaw(DEFAULT_RANGE);
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
it('render default values when closed, utc and relative time range', () => {
const range = fromRaw(DEFAULT_RANGE);
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
it('renders default values when open and relative range', () => {
const range = fromRaw(DEFAULT_RANGE);
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
it('renders default values when open, utc and relative range', () => {
const range = fromRaw(DEFAULT_RANGE);
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
const expectedRangeString = rangeUtil.describeTimeRange(localRange);
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
},
};
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
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('', () => {
const range = fromRaw(rawRange);
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
};
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
};
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
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('', () => {
};
const onChangeTime = sinon.spy();
- const wrapper = shallow();
+ const wrapper = shallow();
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx
index 9a10edc0625..510646dcfe4 100644
--- a/public/app/features/explore/TimePicker.tsx
+++ b/public/app/features/explore/TimePicker.tsx
@@ -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 {
+ handleChangeFrom = (event: ChangeEvent) => {
this.setState({
- fromRaw: e.target.value,
+ fromRaw: event.target.value,
});
};
- handleChangeTo = e => {
+ handleChangeTo = (event: ChangeEvent) => {
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 parseTimeZone(state.timeZone);
+export const getTimeZone = (state: UserState) => state.timeZone;
diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
index 96ff1b4cbc9..501a0829942 100644
--- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
@@ -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",
diff --git a/public/app/plugins/datasource/grafana/specs/datasource.test.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts
index 4c189ac4caa..47202c428cc 100644
--- a/public/app/plugins/datasource/grafana/specs/datasource.test.ts
+++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts
@@ -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');
},
};
diff --git a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap
index e9f83ccafd3..a705b312ea4 100644
--- a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap
+++ b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap
@@ -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",
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 5f7db2de60b..657e687a281 100644
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -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;
diff --git a/public/app/types/user.ts b/public/app/types/user.ts
index 954bf278011..ec3761c7137 100644
--- a/public/app/types/user.ts
+++ b/public/app/types/user.ts
@@ -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 {
diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss
index 9a2f4491750..fb157a92eef 100644
--- a/public/sass/_variables.dark.generated.scss
+++ b/public/sass/_variables.dark.generated.scss
@@ -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;
diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss
index b989af2eb21..c61211fe30a 100644
--- a/public/sass/_variables.light.generated.scss
+++ b/public/sass/_variables.light.generated.scss
@@ -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;
diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss
index 67071bb61c8..434ecbacb1e 100644
--- a/public/sass/components/_navbar.scss
+++ b/public/sass/components/_navbar.scss
@@ -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;
diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss
index bc4cc773c39..48c6384a148 100644
--- a/public/sass/components/_panel_graph.scss
+++ b/public/sass/components/_panel_graph.scss
@@ -304,7 +304,7 @@
}
.graph-annotation__header {
- background-color: $popover-border-color;
+ background: $popover-header-bg;
padding: 6px 10px;
display: flex;
}
diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss
index 3c6ffd83a6f..b0156ce9cee 100644
--- a/public/sass/components/_panel_logs.scss
+++ b/public/sass/components/_panel_logs.scss
@@ -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;
}
diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts
index 648b73de400..aeef0404582 100644
--- a/public/test/specs/helpers.ts
+++ b/public/test/specs/helpers.ts
@@ -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);
},
};
diff --git a/public/vendor/flot/jquery.flot.pie.js b/public/vendor/flot/jquery.flot.pie.js
index dee47e6e504..e69de29bb2d 100644
--- a/public/vendor/flot/jquery.flot.pie.js
+++ b/public/vendor/flot/jquery.flot.pie.js
@@ -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("Could not draw pie with labels contained inside canvas
");
- }
-
- 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 = "" + text + "";
- 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;";
- $("")
- .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 "" + label + "
" + Math.round(slice.percent) + "%
";
- }, // 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);