mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Support user timezone (#16469)
Explore now uses the timezone of the user to decide if local browser time or UTC should be used. - Now uses TimeRange instead of RawTimeRange in explore item state tree and only parsing actual time in a few action handlers. - Time picker should now properly handle moving back/forward and apply time range when both utc and non utc time zone. - URL range representation is changed from YYYY-MM-DD HH:mm:ss to epoch ms. - Now uses AbsoluteTimeRange in graph component instead of moment. - Makes a copy of the time range passed to timeSrv to make sure immutability of explore time range when for example elasticsearch test datasources uses timeSrv and sets a time range of last 1 min. - Various refactorings and cleanup. Closes #12812
This commit is contained in:
committed by
GitHub
parent
7dbe719fda
commit
02cb7ff436
@@ -11,11 +11,30 @@ export interface TimeRange {
|
|||||||
raw: RawTimeRange;
|
raw: RawTimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AbsoluteTimeRange {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IntervalValues {
|
export interface IntervalValues {
|
||||||
interval: string; // 10s,5m
|
interval: string; // 10s,5m
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TimeZone {
|
||||||
|
raw: string;
|
||||||
|
isUtc: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseTimeZone = (raw: string): TimeZone => {
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
isUtc: raw === 'utc',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultTimeZone = parseTimeZone('browser');
|
||||||
|
|
||||||
export interface TimeOption {
|
export interface TimeOption {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import moment, { Moment } from 'moment';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import { renderUrl } from 'app/core/utils/url';
|
import { renderUrl } from 'app/core/utils/url';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { parse as parseDate } from 'app/core/utils/datemath';
|
|
||||||
import { colors } from '@grafana/ui';
|
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
import { getNextRefIdChar } from './query';
|
import { getNextRefIdChar } from './query';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui';
|
import { colors, TimeRange, RawTimeRange, TimeZone, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import {
|
import {
|
||||||
ExploreUrlState,
|
ExploreUrlState,
|
||||||
@@ -104,7 +103,7 @@ export function buildQueryTransaction(
|
|||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
resultType: ResultType,
|
resultType: ResultType,
|
||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
range: RawTimeRange,
|
range: TimeRange,
|
||||||
queryIntervals: QueryIntervals,
|
queryIntervals: QueryIntervals,
|
||||||
scanning: boolean
|
scanning: boolean
|
||||||
): QueryTransaction {
|
): QueryTransaction {
|
||||||
@@ -131,12 +130,8 @@ export function buildQueryTransaction(
|
|||||||
intervalMs,
|
intervalMs,
|
||||||
panelId,
|
panelId,
|
||||||
targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
|
targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
|
||||||
range: {
|
range,
|
||||||
from: dateMath.parse(range.from, false),
|
rangeRaw: range.raw,
|
||||||
to: dateMath.parse(range.to, true),
|
|
||||||
raw: range,
|
|
||||||
},
|
|
||||||
rangeRaw: range,
|
|
||||||
scopedVars: {
|
scopedVars: {
|
||||||
__interval: { text: interval, value: interval },
|
__interval: { text: interval, value: interval },
|
||||||
__interval_ms: { text: intervalMs, value: intervalMs },
|
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||||
@@ -315,17 +310,12 @@ export function calculateResultsFromQueryTransactions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
|
export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues {
|
||||||
if (!resolution) {
|
if (!resolution) {
|
||||||
return { interval: '1s', intervalMs: 1000 };
|
return { interval: '1s', intervalMs: 1000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const absoluteRange: RawTimeRange = {
|
return kbn.calculateInterval(range, resolution, lowLimit);
|
||||||
from: parseDate(range.from, false),
|
|
||||||
to: parseDate(range.to, true),
|
|
||||||
};
|
|
||||||
|
|
||||||
return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
|
export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
|
||||||
@@ -395,3 +385,51 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourc
|
|||||||
|
|
||||||
return queryKeys;
|
return queryKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
raw: rawRange,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawTime = (value): Moment | string => {
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.indexOf('now') !== -1) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value.length === 8) {
|
||||||
|
return moment.utc(value, 'YYYYMMDD');
|
||||||
|
}
|
||||||
|
if (value.length === 15) {
|
||||||
|
return moment.utc(value, 'YYYYMMDDTHHmmss');
|
||||||
|
}
|
||||||
|
// Backward compatibility
|
||||||
|
if (value.length === 19) {
|
||||||
|
return moment.utc(value, 'YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
const epoch = parseInt(value, 10);
|
||||||
|
return moment.utc(epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => {
|
||||||
|
const raw = {
|
||||||
|
from: parseRawTime(range.from),
|
||||||
|
to: parseRawTime(range.to),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: dateMath.parse(raw.from, false, timeZone.raw as any),
|
||||||
|
to: dateMath.parse(raw.to, true, timeZone.raw as any),
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import GraphContainer from './GraphContainer';
|
|||||||
import LogsContainer from './LogsContainer';
|
import LogsContainer from './LogsContainer';
|
||||||
import QueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
import TableContainer from './TableContainer';
|
import TableContainer from './TableContainer';
|
||||||
import TimePicker, { parseTime } from './TimePicker';
|
import TimePicker from './TimePicker';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {
|
import {
|
||||||
@@ -31,15 +31,29 @@ import {
|
|||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
|
import { RawTimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
|
||||||
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId, ExploreUpdateState } from 'app/types/explore';
|
import {
|
||||||
|
ExploreItemState,
|
||||||
|
ExploreUrlState,
|
||||||
|
RangeScanner,
|
||||||
|
ExploreId,
|
||||||
|
ExploreUpdateState,
|
||||||
|
ExploreUIState,
|
||||||
|
} from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
|
import {
|
||||||
|
LAST_USED_DATASOURCE_KEY,
|
||||||
|
ensureQueries,
|
||||||
|
DEFAULT_RANGE,
|
||||||
|
DEFAULT_UI_STATE,
|
||||||
|
getTimeRangeFromUrl,
|
||||||
|
} from 'app/core/utils/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
import { scanStopAction } from './state/actionTypes';
|
import { scanStopAction } from './state/actionTypes';
|
||||||
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
||||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||||
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
|
||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
StartPage?: ComponentClass<ExploreStartPageProps>;
|
||||||
@@ -53,7 +67,6 @@ interface ExploreProps {
|
|||||||
initializeExplore: typeof initializeExplore;
|
initializeExplore: typeof initializeExplore;
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
range: RawTimeRange;
|
|
||||||
update: ExploreUpdateState;
|
update: ExploreUpdateState;
|
||||||
reconnectDatasource: typeof reconnectDatasource;
|
reconnectDatasource: typeof reconnectDatasource;
|
||||||
refreshExplore: typeof refreshExplore;
|
refreshExplore: typeof refreshExplore;
|
||||||
@@ -69,7 +82,10 @@ interface ExploreProps {
|
|||||||
supportsLogs: boolean | null;
|
supportsLogs: boolean | null;
|
||||||
supportsTable: boolean | null;
|
supportsTable: boolean | null;
|
||||||
queryKeys: string[];
|
queryKeys: string[];
|
||||||
urlState: ExploreUrlState;
|
initialDatasource: string;
|
||||||
|
initialQueries: DataQuery[];
|
||||||
|
initialRange: RawTimeRange;
|
||||||
|
initialUI: ExploreUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,12 +127,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { exploreId, urlState, initialized } = this.props;
|
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, initialUI } = this.props;
|
||||||
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
|
|
||||||
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
|
||||||
const initialQueries: DataQuery[] = ensureQueries(queries);
|
|
||||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
|
||||||
const width = this.el ? this.el.offsetWidth : 0;
|
const width = this.el ? this.el.offsetWidth : 0;
|
||||||
|
|
||||||
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
this.props.initializeExplore(
|
this.props.initializeExplore(
|
||||||
@@ -126,7 +139,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
initialRange,
|
initialRange,
|
||||||
width,
|
width,
|
||||||
this.exploreEvents,
|
this.exploreEvents,
|
||||||
ui
|
initialUI
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +156,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
this.el = el;
|
this.el = el;
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
|
onChangeTime = (range: RawTimeRange, changedByScanner?: boolean) => {
|
||||||
if (this.props.scanning && !changedByScanner) {
|
if (this.props.scanning && !changedByScanner) {
|
||||||
this.onStopScanning();
|
this.onStopScanning();
|
||||||
}
|
}
|
||||||
@@ -286,6 +299,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const { split } = explore;
|
const { split } = explore;
|
||||||
const item: ExploreItemState = explore[exploreId];
|
const item: ExploreItemState = explore[exploreId];
|
||||||
|
const timeZone = getTimeZone(state.user);
|
||||||
const {
|
const {
|
||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
@@ -293,7 +307,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
datasourceLoading,
|
datasourceLoading,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
range,
|
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
supportsGraph,
|
supportsGraph,
|
||||||
supportsLogs,
|
supportsLogs,
|
||||||
@@ -302,6 +315,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
urlState,
|
urlState,
|
||||||
update,
|
update,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
|
const { datasource, queries, range: urlRange, ui } = (urlState || {}) as ExploreUrlState;
|
||||||
|
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
||||||
|
const initialQueries: DataQuery[] = ensureQueries(queries);
|
||||||
|
const initialRange = urlRange ? getTimeRangeFromUrl(urlRange, timeZone).raw : DEFAULT_RANGE;
|
||||||
|
const initialUI = ui || DEFAULT_UI_STATE;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
@@ -309,15 +329,17 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
datasourceLoading,
|
datasourceLoading,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
range,
|
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
split,
|
split,
|
||||||
supportsGraph,
|
supportsGraph,
|
||||||
supportsLogs,
|
supportsLogs,
|
||||||
supportsTable,
|
supportsTable,
|
||||||
queryKeys,
|
queryKeys,
|
||||||
urlState,
|
|
||||||
update,
|
update,
|
||||||
|
initialDatasource,
|
||||||
|
initialQueries,
|
||||||
|
initialRange,
|
||||||
|
initialUI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
|
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import { DataSourceSelectItem, RawTimeRange, TimeRange, ClickOutsideWrapper } from '@grafana/ui';
|
import { DataSourceSelectItem, RawTimeRange, ClickOutsideWrapper, TimeZone, TimeRange } from '@grafana/ui';
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
changeRefreshInterval,
|
changeRefreshInterval,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import TimePicker from './TimePicker';
|
import TimePicker from './TimePicker';
|
||||||
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
import { RefreshPicker, SetInterval } from '@grafana/ui';
|
import { RefreshPicker, SetInterval } from '@grafana/ui';
|
||||||
|
|
||||||
enum IconSide {
|
enum IconSide {
|
||||||
@@ -48,14 +49,15 @@ const createResponsiveButton = (options: {
|
|||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
timepickerRef: React.RefObject<TimePicker>;
|
timepickerRef: React.RefObject<TimePicker>;
|
||||||
onChangeTime: (range: TimeRange, changedByScanner?: boolean) => void;
|
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
datasourceMissing: boolean;
|
datasourceMissing: boolean;
|
||||||
exploreDatasources: DataSourceSelectItem[];
|
exploreDatasources: DataSourceSelectItem[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
|
timeZone: TimeZone;
|
||||||
selectedDatasource: DataSourceSelectItem;
|
selectedDatasource: DataSourceSelectItem;
|
||||||
splitted: boolean;
|
splitted: boolean;
|
||||||
refreshInterval: string;
|
refreshInterval: string;
|
||||||
@@ -106,6 +108,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
exploreId,
|
exploreId,
|
||||||
loading,
|
loading,
|
||||||
range,
|
range,
|
||||||
|
timeZone,
|
||||||
selectedDatasource,
|
selectedDatasource,
|
||||||
splitted,
|
splitted,
|
||||||
timepickerRef,
|
timepickerRef,
|
||||||
@@ -159,7 +162,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="explore-toolbar-content-item timepicker">
|
<div className="explore-toolbar-content-item timepicker">
|
||||||
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
|
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
|
||||||
<TimePicker ref={timepickerRef} range={range} onChangeTime={onChangeTime} />
|
<TimePicker ref={timepickerRef} range={range} isUtc={timeZone.isUtc} onChangeTime={onChangeTime} />
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
|
|
||||||
<RefreshPicker
|
<RefreshPicker
|
||||||
@@ -214,6 +217,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
exploreDatasources,
|
exploreDatasources,
|
||||||
loading,
|
loading,
|
||||||
range,
|
range,
|
||||||
|
timeZone: getTimeZone(state.user),
|
||||||
selectedDatasource,
|
selectedDatasource,
|
||||||
splitted,
|
splitted,
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Graph } from './Graph';
|
import { Graph } from './Graph';
|
||||||
import { mockData } from './__mocks__/mockData';
|
import { mockData } from './__mocks__/mockData';
|
||||||
|
import { DefaultTimeZone } from '@grafana/ui';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props = {
|
const props = {
|
||||||
size: { width: 10, height: 20 },
|
size: { width: 10, height: 20 },
|
||||||
data: mockData().slice(0, 19),
|
data: mockData().slice(0, 19),
|
||||||
range: { from: 'now-6h', to: 'now' },
|
range: { from: 0, to: 1 },
|
||||||
|
timeZone: DefaultTimeZone,
|
||||||
...propOverrides,
|
...propOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
import 'vendor/flot/jquery.flot';
|
import 'vendor/flot/jquery.flot';
|
||||||
import 'vendor/flot/jquery.flot.time';
|
import 'vendor/flot/jquery.flot.time';
|
||||||
import 'vendor/flot/jquery.flot.selection';
|
import 'vendor/flot/jquery.flot.selection';
|
||||||
import 'vendor/flot/jquery.flot.stack';
|
import 'vendor/flot/jquery.flot.stack';
|
||||||
|
|
||||||
import { RawTimeRange } from '@grafana/ui';
|
import { TimeZone, AbsoluteTimeRange } from '@grafana/ui';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
|
||||||
import Legend from './Legend';
|
import Legend from './Legend';
|
||||||
@@ -78,10 +76,11 @@ interface GraphProps {
|
|||||||
height?: number;
|
height?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
range: RawTimeRange;
|
range: AbsoluteTimeRange;
|
||||||
|
timeZone: TimeZone;
|
||||||
split?: boolean;
|
split?: boolean;
|
||||||
userOptions?: any;
|
userOptions?: any;
|
||||||
onChangeTime?: (range: RawTimeRange) => void;
|
onChangeTime?: (range: AbsoluteTimeRange) => void;
|
||||||
onToggleSeries?: (alias: string, hiddenSeries: Set<string>) => void;
|
onToggleSeries?: (alias: string, hiddenSeries: Set<string>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,27 +132,20 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPlotSelected = (event, ranges) => {
|
onPlotSelected = (event, ranges) => {
|
||||||
if (this.props.onChangeTime) {
|
const { onChangeTime } = this.props;
|
||||||
const range = {
|
if (onChangeTime) {
|
||||||
from: moment(ranges.xaxis.from),
|
this.props.onChangeTime({
|
||||||
to: moment(ranges.xaxis.to),
|
from: ranges.xaxis.from,
|
||||||
};
|
to: ranges.xaxis.to,
|
||||||
this.props.onChangeTime(range);
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getDynamicOptions() {
|
getDynamicOptions() {
|
||||||
const { range, width } = this.props;
|
const { range, width, timeZone } = this.props;
|
||||||
const ticks = (width || 0) / 100;
|
const ticks = (width || 0) / 100;
|
||||||
let { from, to } = range;
|
const min = range.from;
|
||||||
if (!moment.isMoment(from)) {
|
const max = range.to;
|
||||||
from = dateMath.parse(from, false);
|
|
||||||
}
|
|
||||||
if (!moment.isMoment(to)) {
|
|
||||||
to = dateMath.parse(to, true);
|
|
||||||
}
|
|
||||||
const min = from.valueOf();
|
|
||||||
const max = to.valueOf();
|
|
||||||
return {
|
return {
|
||||||
xaxis: {
|
xaxis: {
|
||||||
mode: 'time',
|
mode: 'time',
|
||||||
@@ -161,7 +153,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
max: max,
|
max: max,
|
||||||
label: 'Datetime',
|
label: 'Datetime',
|
||||||
ticks: ticks,
|
ticks: ticks,
|
||||||
timezone: 'browser',
|
timezone: timeZone.raw,
|
||||||
timeformat: time_format(ticks, min, max),
|
timeformat: time_format(ticks, min, max),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { TimeRange, RawTimeRange } from '@grafana/ui';
|
import moment from 'moment';
|
||||||
|
import { TimeRange, TimeZone, AbsoluteTimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
@@ -9,12 +10,14 @@ import { StoreState } from 'app/types';
|
|||||||
import { toggleGraph, changeTime } from './state/actions';
|
import { toggleGraph, changeTime } from './state/actions';
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
import Panel from './Panel';
|
import Panel from './Panel';
|
||||||
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
|
||||||
interface GraphContainerProps {
|
interface GraphContainerProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
graphResult?: any[];
|
graphResult?: any[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
|
timeZone: TimeZone;
|
||||||
showingGraph: boolean;
|
showingGraph: boolean;
|
||||||
showingTable: boolean;
|
showingTable: boolean;
|
||||||
split: boolean;
|
split: boolean;
|
||||||
@@ -28,13 +31,20 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
|
|||||||
this.props.toggleGraph(this.props.exploreId, this.props.showingGraph);
|
this.props.toggleGraph(this.props.exploreId, this.props.showingGraph);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTime = (timeRange: TimeRange) => {
|
onChangeTime = (absRange: AbsoluteTimeRange) => {
|
||||||
this.props.changeTime(this.props.exploreId, timeRange);
|
const { exploreId, timeZone, changeTime } = this.props;
|
||||||
|
const range = {
|
||||||
|
from: timeZone.isUtc ? moment.utc(absRange.from) : moment(absRange.from),
|
||||||
|
to: timeZone.isUtc ? moment.utc(absRange.to) : moment(absRange.to),
|
||||||
|
};
|
||||||
|
|
||||||
|
changeTime(exploreId, range);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width } = this.props;
|
const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width, timeZone } = this.props;
|
||||||
const graphHeight = showingGraph && showingTable ? 200 : 400;
|
const graphHeight = showingGraph && showingTable ? 200 : 400;
|
||||||
|
const timeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
|
||||||
|
|
||||||
if (!graphResult) {
|
if (!graphResult) {
|
||||||
return null;
|
return null;
|
||||||
@@ -47,7 +57,8 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
|
|||||||
height={graphHeight}
|
height={graphHeight}
|
||||||
id={`explore-graph-${exploreId}`}
|
id={`explore-graph-${exploreId}`}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
range={range}
|
range={timeRange}
|
||||||
|
timeZone={timeZone}
|
||||||
split={split}
|
split={split}
|
||||||
width={width}
|
width={width}
|
||||||
/>
|
/>
|
||||||
@@ -62,7 +73,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
const item: ExploreItemState = explore[exploreId];
|
const item: ExploreItemState = explore[exploreId];
|
||||||
const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
|
const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
|
||||||
const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
|
const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
|
||||||
return { graphResult, loading, range, showingGraph, showingTable, split };
|
return { graphResult, loading, range, showingGraph, showingTable, split, timeZone: getTimeZone(state.user) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import { RawTimeRange, Switch, LogLevel } from '@grafana/ui';
|
import { RawTimeRange, Switch, LogLevel, TimeZone, TimeRange, AbsoluteTimeRange } from '@grafana/ui';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
|
||||||
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
|
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
|
||||||
@@ -48,12 +48,13 @@ interface Props {
|
|||||||
exploreId: string;
|
exploreId: string;
|
||||||
highlighterExpressions: string[];
|
highlighterExpressions: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
range?: RawTimeRange;
|
range: TimeRange;
|
||||||
|
timeZone: TimeZone;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
dedupStrategy: LogsDedupStrategy;
|
dedupStrategy: LogsDedupStrategy;
|
||||||
hiddenLogLevels: Set<LogLevel>;
|
hiddenLogLevels: Set<LogLevel>;
|
||||||
onChangeTime?: (range: RawTimeRange) => void;
|
onChangeTime?: (range: AbsoluteTimeRange) => void;
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
onStartScanning?: () => void;
|
onStartScanning?: () => void;
|
||||||
onStopScanning?: () => void;
|
onStopScanning?: () => void;
|
||||||
@@ -156,6 +157,7 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
loading = false,
|
loading = false,
|
||||||
onClickLabel,
|
onClickLabel,
|
||||||
range,
|
range,
|
||||||
|
timeZone,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
width,
|
width,
|
||||||
@@ -191,6 +193,10 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
||||||
const getRows = () => processedRows;
|
const getRows = () => processedRows;
|
||||||
const timeSeries = data.series.map(series => new TimeSeries(series));
|
const timeSeries = data.series.map(series => new TimeSeries(series));
|
||||||
|
const absRange = {
|
||||||
|
from: range.from.valueOf(),
|
||||||
|
to: range.to.valueOf(),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-panel">
|
<div className="logs-panel">
|
||||||
@@ -199,7 +205,8 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
data={timeSeries}
|
data={timeSeries}
|
||||||
height={100}
|
height={100}
|
||||||
width={width}
|
width={width}
|
||||||
range={range}
|
range={absRange}
|
||||||
|
timeZone={timeZone}
|
||||||
id={`explore-logs-graph-${exploreId}`}
|
id={`explore-logs-graph-${exploreId}`}
|
||||||
onChangeTime={this.props.onChangeTime}
|
onChangeTime={this.props.onChangeTime}
|
||||||
onToggleSeries={this.onToggleLogLevel}
|
onToggleSeries={this.onToggleLogLevel}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { RawTimeRange, TimeRange, LogLevel } from '@grafana/ui';
|
import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
@@ -12,6 +12,7 @@ import Logs from './Logs';
|
|||||||
import Panel from './Panel';
|
import Panel from './Panel';
|
||||||
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
||||||
import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors';
|
import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors';
|
||||||
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
|
||||||
interface LogsContainerProps {
|
interface LogsContainerProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
@@ -19,11 +20,12 @@ interface LogsContainerProps {
|
|||||||
logsHighlighterExpressions?: string[];
|
logsHighlighterExpressions?: string[];
|
||||||
logsResult?: LogsModel;
|
logsResult?: LogsModel;
|
||||||
dedupedResult?: LogsModel;
|
dedupedResult?: LogsModel;
|
||||||
onChangeTime: (range: TimeRange) => void;
|
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||||
onClickLabel: (key: string, value: string) => void;
|
onClickLabel: (key: string, value: string) => void;
|
||||||
onStartScanning: () => void;
|
onStartScanning: () => void;
|
||||||
onStopScanning: () => void;
|
onStopScanning: () => void;
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
|
timeZone: TimeZone;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
showingLogs: boolean;
|
showingLogs: boolean;
|
||||||
@@ -64,6 +66,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
onStartScanning,
|
onStartScanning,
|
||||||
onStopScanning,
|
onStopScanning,
|
||||||
range,
|
range,
|
||||||
|
timeZone,
|
||||||
showingLogs,
|
showingLogs,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
@@ -88,6 +91,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
onDedupStrategyChange={this.handleDedupStrategyChange}
|
onDedupStrategyChange={this.handleDedupStrategyChange}
|
||||||
onToggleLogLevel={this.hangleToggleLogLevel}
|
onToggleLogLevel={this.hangleToggleLogLevel}
|
||||||
range={range}
|
range={range}
|
||||||
|
timeZone={timeZone}
|
||||||
scanning={scanning}
|
scanning={scanning}
|
||||||
scanRange={scanRange}
|
scanRange={scanRange}
|
||||||
width={width}
|
width={width}
|
||||||
@@ -106,6 +110,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item);
|
const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item);
|
||||||
const hiddenLogLevels = new Set(item.hiddenLogLevels);
|
const hiddenLogLevels = new Set(item.hiddenLogLevels);
|
||||||
const dedupedResult = deduplicatedLogsSelector(item);
|
const dedupedResult = deduplicatedLogsSelector(item);
|
||||||
|
const timeZone = getTimeZone(state.user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
@@ -115,6 +120,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
scanRange,
|
scanRange,
|
||||||
showingLogs,
|
showingLogs,
|
||||||
range,
|
range,
|
||||||
|
timeZone,
|
||||||
dedupStrategy,
|
dedupStrategy,
|
||||||
hiddenLogLevels,
|
hiddenLogLevels,
|
||||||
dedupedResult,
|
dedupedResult,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
@@ -7,7 +8,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { RawTimeRange, DataQuery } from '@grafana/ui';
|
import { DataQuery, TimeRange } from '@grafana/ui';
|
||||||
import 'app/features/plugins/plugin_loader';
|
import 'app/features/plugins/plugin_loader';
|
||||||
|
|
||||||
interface QueryEditorProps {
|
interface QueryEditorProps {
|
||||||
@@ -17,7 +18,7 @@ interface QueryEditorProps {
|
|||||||
onQueryChange?: (value: DataQuery) => void;
|
onQueryChange?: (value: DataQuery) => void;
|
||||||
initialQuery: DataQuery;
|
initialQuery: DataQuery;
|
||||||
exploreEvents: Emitter;
|
exploreEvents: Emitter;
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||||
@@ -62,10 +63,13 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initTimeSrv(range) {
|
initTimeSrv(range: TimeRange) {
|
||||||
const timeSrv = getTimeSrv();
|
const timeSrv = getTimeSrv();
|
||||||
timeSrv.init({
|
timeSrv.init({
|
||||||
time: range,
|
time: {
|
||||||
|
from: moment(range.from),
|
||||||
|
to: moment(range.to),
|
||||||
|
},
|
||||||
refresh: false,
|
refresh: false,
|
||||||
getTimezone: () => 'utc',
|
getTimezone: () => 'utc',
|
||||||
timeRangeUpdated: () => console.log('refreshDashboard!'),
|
timeRangeUpdated: () => console.log('refreshDashboard!'),
|
||||||
|
|||||||
@@ -14,14 +14,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import {
|
import { DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction, DataSourceStatus, TimeRange } from '@grafana/ui';
|
||||||
RawTimeRange,
|
|
||||||
DataQuery,
|
|
||||||
ExploreDataSourceApi,
|
|
||||||
QueryHint,
|
|
||||||
QueryFixAction,
|
|
||||||
DataSourceStatus,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
|
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
|
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
|
||||||
@@ -48,7 +41,7 @@ interface QueryRowProps {
|
|||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
queryTransactions: QueryTransaction[];
|
queryTransactions: QueryTransaction[];
|
||||||
exploreEvents: Emitter;
|
exploreEvents: Emitter;
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
removeQueryRowAction: typeof removeQueryRowAction;
|
removeQueryRowAction: typeof removeQueryRowAction;
|
||||||
runQueries: typeof runQueries;
|
runQueries: typeof runQueries;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,238 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import TimePicker, { DEFAULT_RANGE, parseTime } from './TimePicker';
|
import TimePicker from './TimePicker';
|
||||||
|
import { RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui';
|
||||||
|
|
||||||
|
const DEFAULT_RANGE = {
|
||||||
|
from: 'now-6h',
|
||||||
|
to: 'now',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromRaw = (rawRange: RawTimeRange): TimeRange => {
|
||||||
|
const raw = {
|
||||||
|
from: moment.isMoment(rawRange.from) ? moment(rawRange.from) : rawRange.from,
|
||||||
|
to: moment.isMoment(rawRange.to) ? moment(rawRange.to) : rawRange.to,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: dateMath.parse(raw.from, false),
|
||||||
|
to: dateMath.parse(raw.to, true),
|
||||||
|
raw: rawRange,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
describe('<TimePicker />', () => {
|
describe('<TimePicker />', () => {
|
||||||
it('renders closed with default values', () => {
|
it('render default values when closed and relative time range', () => {
|
||||||
const rangeString = rangeUtil.describeTimeRange(DEFAULT_RANGE);
|
const range = fromRaw(DEFAULT_RANGE);
|
||||||
const wrapper = shallow(<TimePicker />);
|
const wrapper = shallow(<TimePicker range={range} />);
|
||||||
expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
|
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
|
||||||
expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBe(false);
|
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
|
||||||
|
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
|
||||||
|
expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeFalsy();
|
||||||
|
expect(wrapper.find('.gf-timepicker-utc').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with relative range', () => {
|
it('render default values when closed, utc and relative time range', () => {
|
||||||
const range = {
|
const range = fromRaw(DEFAULT_RANGE);
|
||||||
from: 'now-7h',
|
const wrapper = shallow(<TimePicker range={range} isUtc />);
|
||||||
to: 'now',
|
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
|
||||||
};
|
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
|
||||||
const rangeString = rangeUtil.describeTimeRange(range);
|
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
|
||||||
|
expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeFalsy();
|
||||||
|
expect(wrapper.find('.gf-timepicker-utc').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 />);
|
||||||
expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
|
expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from);
|
||||||
expect(wrapper.state('fromRaw')).toBe(range.from);
|
expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to);
|
||||||
expect(wrapper.state('toRaw')).toBe(range.to);
|
expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours');
|
||||||
expect(wrapper.find('.timepicker-from').props().value).toBe(range.from);
|
expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeTruthy();
|
||||||
expect(wrapper.find('.timepicker-to').props().value).toBe(range.to);
|
expect(wrapper.find('.gf-timepicker-utc').exists()).toBeFalsy();
|
||||||
|
expect(wrapper.find('.timepicker-from').props().value).toBe(DEFAULT_RANGE.from);
|
||||||
|
expect(wrapper.find('.timepicker-to').props().value).toBe(DEFAULT_RANGE.to);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with epoch (millies) range converted to ISO-ish', () => {
|
it('renders default values when open, utc and relative range', () => {
|
||||||
|
const range = fromRaw(DEFAULT_RANGE);
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isOpen isUtc />);
|
||||||
|
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');
|
||||||
|
expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeTruthy();
|
||||||
|
expect(wrapper.find('.gf-timepicker-utc').exists()).toBeTruthy();
|
||||||
|
expect(wrapper.find('.timepicker-from').props().value).toBe(DEFAULT_RANGE.from);
|
||||||
|
expect(wrapper.find('.timepicker-to').props().value).toBe(DEFAULT_RANGE.to);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('apply with absolute range and non-utc', () => {
|
||||||
const range = {
|
const range = {
|
||||||
from: '1',
|
from: moment.utc(1),
|
||||||
to: '1000',
|
to: moment.utc(1000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(1),
|
||||||
|
to: moment.utc(1000),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const rangeString = rangeUtil.describeTimeRange({
|
const localRange = {
|
||||||
from: parseTime(range.from, true),
|
from: moment(1),
|
||||||
to: parseTime(range.to, true),
|
to: moment(1000),
|
||||||
});
|
raw: {
|
||||||
const wrapper = shallow(<TimePicker range={range} isUtc isOpen />);
|
from: moment(1),
|
||||||
|
to: moment(1000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedRangeString = rangeUtil.describeTimeRange(localRange);
|
||||||
|
|
||||||
|
const onChangeTime = sinon.spy();
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
|
||||||
|
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);
|
||||||
|
expect(wrapper.find('.timepicker-rangestring').text()).toBe(expectedRangeString);
|
||||||
|
expect(wrapper.find('.timepicker-from').props().value).toBe(localRange.from.format(TIME_FORMAT));
|
||||||
|
expect(wrapper.find('.timepicker-to').props().value).toBe(localRange.to.format(TIME_FORMAT));
|
||||||
|
|
||||||
|
wrapper.find('button.gf-form-btn').simulate('click');
|
||||||
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(0);
|
||||||
|
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000);
|
||||||
|
|
||||||
|
expect(wrapper.state('isOpen')).toBeFalsy();
|
||||||
|
expect(wrapper.state('rangeString')).toBe(expectedRangeString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('apply with absolute range and utc', () => {
|
||||||
|
const range = {
|
||||||
|
from: moment.utc(1),
|
||||||
|
to: moment.utc(1000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(1),
|
||||||
|
to: moment.utc(1000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const onChangeTime = sinon.spy();
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
|
||||||
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00');
|
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('toRaw')).toBe('1970-01-01 00:00:01');
|
||||||
expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
|
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-from').props().value).toBe('1970-01-01 00:00:00');
|
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');
|
expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01');
|
||||||
|
|
||||||
|
wrapper.find('button.gf-form-btn').simulate('click');
|
||||||
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(0);
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves ranges forward and backward by half the range on arrow click', () => {
|
it('moves ranges backward by half the range on left arrow click when utc', () => {
|
||||||
const range = {
|
const rawRange = {
|
||||||
from: '2000',
|
from: moment.utc(2000),
|
||||||
to: '4000',
|
to: moment.utc(4000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(2000),
|
||||||
|
to: moment.utc(4000),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const rangeString = rangeUtil.describeTimeRange({
|
const range = fromRaw(rawRange);
|
||||||
from: parseTime(range.from, true),
|
|
||||||
to: parseTime(range.to, true),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onChangeTime = sinon.spy();
|
const onChangeTime = sinon.spy();
|
||||||
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
|
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
|
||||||
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
|
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
|
||||||
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
|
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
|
||||||
expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
|
|
||||||
expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:02');
|
|
||||||
expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:04');
|
|
||||||
|
|
||||||
wrapper.find('.timepicker-left').simulate('click');
|
wrapper.find('.timepicker-left').simulate('click');
|
||||||
expect(onChangeTime.calledOnce).toBe(true);
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(1000);
|
||||||
|
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves ranges backward by half the range on left arrow click when not utc', () => {
|
||||||
|
const range = {
|
||||||
|
from: moment.utc(2000),
|
||||||
|
to: moment.utc(4000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(2000),
|
||||||
|
to: moment.utc(4000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const localRange = {
|
||||||
|
from: moment(2000),
|
||||||
|
to: moment(4000),
|
||||||
|
raw: {
|
||||||
|
from: moment(2000),
|
||||||
|
to: moment(4000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeTime = sinon.spy();
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isUtc={false} isOpen onChangeTime={onChangeTime} />);
|
||||||
|
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
|
||||||
|
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
|
||||||
|
|
||||||
|
wrapper.find('.timepicker-left').simulate('click');
|
||||||
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(1000);
|
||||||
|
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves ranges forward by half the range on right arrow click when utc', () => {
|
||||||
|
const range = {
|
||||||
|
from: moment.utc(1000),
|
||||||
|
to: moment.utc(3000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(1000),
|
||||||
|
to: moment.utc(3000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeTime = sinon.spy();
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
|
||||||
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01');
|
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01');
|
||||||
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03');
|
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03');
|
||||||
|
|
||||||
wrapper.find('.timepicker-right').simulate('click');
|
wrapper.find('.timepicker-right').simulate('click');
|
||||||
expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(2000);
|
||||||
|
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves ranges forward by half the range on right arrow click when not utc', () => {
|
||||||
|
const range = {
|
||||||
|
from: moment.utc(1000),
|
||||||
|
to: moment.utc(3000),
|
||||||
|
raw: {
|
||||||
|
from: moment.utc(1000),
|
||||||
|
to: moment.utc(3000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const localRange = {
|
||||||
|
from: moment(1000),
|
||||||
|
to: moment(3000),
|
||||||
|
raw: {
|
||||||
|
from: moment(1000),
|
||||||
|
to: moment(3000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeTime = sinon.spy();
|
||||||
|
const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
|
||||||
|
expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
|
||||||
|
expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
|
||||||
|
|
||||||
|
wrapper.find('.timepicker-right').simulate('click');
|
||||||
|
expect(onChangeTime.calledOnce).toBeTruthy();
|
||||||
|
expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(2000);
|
||||||
|
expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(4000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,43 +1,12 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
|
||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import { Input, RawTimeRange, TimeRange } from '@grafana/ui';
|
import { Input, RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui';
|
||||||
|
|
||||||
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
export const DEFAULT_RANGE = {
|
|
||||||
from: 'now-6h',
|
|
||||||
to: 'now',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a human-editable string of either relative (inludes "now") or absolute local time (in the shape of DATE_FORMAT).
|
|
||||||
* @param value Epoch or relative time
|
|
||||||
*/
|
|
||||||
export function parseTime(value: string | moment.Moment, isUtc = false, ensureString = false): string | moment.Moment {
|
|
||||||
if (moment.isMoment(value)) {
|
|
||||||
if (ensureString) {
|
|
||||||
return value.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if ((value as string).indexOf('now') !== -1) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
let time: any = value;
|
|
||||||
// Possible epoch
|
|
||||||
if (!isNaN(time)) {
|
|
||||||
time = parseInt(time, 10);
|
|
||||||
}
|
|
||||||
time = isUtc ? moment.utc(time) : moment(time);
|
|
||||||
return time.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TimePickerProps {
|
interface TimePickerProps {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
isUtc?: boolean;
|
isUtc?: boolean;
|
||||||
range?: RawTimeRange;
|
range: TimeRange;
|
||||||
onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void;
|
onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,17 +15,40 @@ interface TimePickerState {
|
|||||||
isUtc: boolean;
|
isUtc: boolean;
|
||||||
rangeString: string;
|
rangeString: string;
|
||||||
refreshInterval?: string;
|
refreshInterval?: string;
|
||||||
initialRange?: RawTimeRange;
|
initialRange: RawTimeRange;
|
||||||
|
|
||||||
// Input-controlled text, keep these in a shape that is human-editable
|
// Input-controlled text, keep these in a shape that is human-editable
|
||||||
fromRaw: string;
|
fromRaw: string;
|
||||||
toRaw: string;
|
toRaw: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRaw = (isUtc: boolean, range: any) => {
|
||||||
|
const rawRange = {
|
||||||
|
from: range.raw.from,
|
||||||
|
to: range.raw.to,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (moment.isMoment(rawRange.from)) {
|
||||||
|
if (!isUtc) {
|
||||||
|
rawRange.from = rawRange.from.local();
|
||||||
|
}
|
||||||
|
rawRange.from = rawRange.from.format(TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moment.isMoment(rawRange.to)) {
|
||||||
|
if (!isUtc) {
|
||||||
|
rawRange.to = rawRange.to.local();
|
||||||
|
}
|
||||||
|
rawRange.to = rawRange.to.format(TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawRange;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimePicker with dropdown menu for relative dates.
|
* TimePicker with dropdown menu for relative dates.
|
||||||
*
|
*
|
||||||
* Initialize with a range that is either based on relative time strings,
|
* Initialize with a range that is either based on relative rawRange.strings,
|
||||||
* or on Moment objects.
|
* or on Moment objects.
|
||||||
* Internally the component needs to keep a string representation in `fromRaw`
|
* Internally the component needs to keep a string representation in `fromRaw`
|
||||||
* and `toRaw` for the controlled inputs.
|
* and `toRaw` for the controlled inputs.
|
||||||
@@ -69,89 +61,68 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const { range, isUtc, isOpen } = props;
|
||||||
|
const rawRange = getRaw(props.isUtc, range);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: props.isOpen,
|
isOpen: isOpen,
|
||||||
isUtc: props.isUtc,
|
isUtc: isUtc,
|
||||||
rangeString: '',
|
rangeString: rangeUtil.describeTimeRange(range.raw),
|
||||||
fromRaw: '',
|
fromRaw: rawRange.from,
|
||||||
toRaw: '',
|
toRaw: rawRange.to,
|
||||||
initialRange: DEFAULT_RANGE,
|
initialRange: range.raw,
|
||||||
refreshInterval: '',
|
refreshInterval: '',
|
||||||
};
|
};
|
||||||
} //Temp solution... How do detect if ds supports table format?
|
} //Temp solution... How do detect if ds supports table format?
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props: TimePickerProps, state: TimePickerState) {
|
||||||
if (state.initialRange && state.initialRange === props.range) {
|
if (
|
||||||
|
state.initialRange &&
|
||||||
|
state.initialRange.from === props.range.raw.from &&
|
||||||
|
state.initialRange.to === props.range.raw.to
|
||||||
|
) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = props.range ? props.range.from : DEFAULT_RANGE.from;
|
const { range } = props;
|
||||||
const to = props.range ? props.range.to : DEFAULT_RANGE.to;
|
const rawRange = getRaw(props.isUtc, range);
|
||||||
|
|
||||||
// Ensure internal string format
|
|
||||||
const fromRaw = parseTime(from, props.isUtc, true);
|
|
||||||
const toRaw = parseTime(to, props.isUtc, true);
|
|
||||||
const range = {
|
|
||||||
from: fromRaw,
|
|
||||||
to: toRaw,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fromRaw,
|
fromRaw: rawRange.from,
|
||||||
toRaw,
|
toRaw: rawRange.to,
|
||||||
initialRange: props.range,
|
initialRange: range.raw,
|
||||||
rangeString: rangeUtil.describeTimeRange(range),
|
rangeString: rangeUtil.describeTimeRange(range.raw),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
move(direction: number, scanning?: boolean): RawTimeRange {
|
move(direction: number, scanning?: boolean): RawTimeRange {
|
||||||
const { onChangeTime } = this.props;
|
const { onChangeTime, range: origRange } = this.props;
|
||||||
const { fromRaw, toRaw } = this.state;
|
const range = {
|
||||||
const from = dateMath.parse(fromRaw, false);
|
from: moment.utc(origRange.from),
|
||||||
const to = dateMath.parse(toRaw, true);
|
to: moment.utc(origRange.to),
|
||||||
const step = scanning ? 1 : 2;
|
};
|
||||||
const timespan = (to.valueOf() - from.valueOf()) / step;
|
const timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
|
||||||
|
let to, from;
|
||||||
let nextTo, nextFrom;
|
|
||||||
if (direction === -1) {
|
if (direction === -1) {
|
||||||
nextTo = to.valueOf() - timespan;
|
to = range.to.valueOf() - timespan;
|
||||||
nextFrom = from.valueOf() - timespan;
|
from = range.from.valueOf() - timespan;
|
||||||
} else if (direction === 1) {
|
} else if (direction === 1) {
|
||||||
nextTo = to.valueOf() + timespan;
|
to = range.to.valueOf() + timespan;
|
||||||
nextFrom = from.valueOf() + timespan;
|
from = range.from.valueOf() + timespan;
|
||||||
if (nextTo > Date.now() && to.valueOf() < Date.now()) {
|
|
||||||
nextTo = Date.now();
|
|
||||||
nextFrom = from.valueOf();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
nextTo = to.valueOf();
|
to = range.to.valueOf();
|
||||||
nextFrom = from.valueOf();
|
from = range.from.valueOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextRange = {
|
const nextTimeRange = {
|
||||||
from: moment(nextFrom),
|
from: this.props.isUtc ? moment.utc(from) : moment(from),
|
||||||
to: moment(nextTo),
|
to: this.props.isUtc ? moment.utc(to) : moment(to),
|
||||||
};
|
};
|
||||||
|
if (onChangeTime) {
|
||||||
const nextTimeRange: TimeRange = {
|
onChangeTime(nextTimeRange);
|
||||||
raw: nextRange,
|
}
|
||||||
from: nextRange.from,
|
return nextTimeRange;
|
||||||
to: nextRange.to,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
rangeString: rangeUtil.describeTimeRange(nextRange),
|
|
||||||
fromRaw: nextRange.from.format(DATE_FORMAT),
|
|
||||||
toRaw: nextRange.to.format(DATE_FORMAT),
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
onChangeTime(nextTimeRange, scanning);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextRange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeFrom = e => {
|
handleChangeFrom = e => {
|
||||||
@@ -167,16 +138,25 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleClickApply = () => {
|
handleClickApply = () => {
|
||||||
const { onChangeTime } = this.props;
|
const { onChangeTime, isUtc } = this.props;
|
||||||
let range;
|
let rawRange;
|
||||||
this.setState(
|
this.setState(
|
||||||
state => {
|
state => {
|
||||||
const { toRaw, fromRaw } = this.state;
|
const { toRaw, fromRaw } = this.state;
|
||||||
range = {
|
rawRange = {
|
||||||
from: dateMath.parse(fromRaw, false),
|
from: fromRaw,
|
||||||
to: dateMath.parse(toRaw, true),
|
to: toRaw,
|
||||||
};
|
};
|
||||||
const rangeString = rangeUtil.describeTimeRange(range);
|
|
||||||
|
if (rawRange.from.indexOf('now') === -1) {
|
||||||
|
rawRange.from = isUtc ? moment.utc(rawRange.from, TIME_FORMAT) : moment(rawRange.from, TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawRange.to.indexOf('now') === -1) {
|
||||||
|
rawRange.to = isUtc ? moment.utc(rawRange.to, TIME_FORMAT) : moment(rawRange.to, TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeString = rangeUtil.describeTimeRange(rawRange);
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
rangeString,
|
rangeString,
|
||||||
@@ -184,7 +164,7 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
if (onChangeTime) {
|
if (onChangeTime) {
|
||||||
onChangeTime(range);
|
onChangeTime(rawRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -201,16 +181,20 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
|||||||
handleClickRelativeOption = range => {
|
handleClickRelativeOption = range => {
|
||||||
const { onChangeTime } = this.props;
|
const { onChangeTime } = this.props;
|
||||||
const rangeString = rangeUtil.describeTimeRange(range);
|
const rangeString = rangeUtil.describeTimeRange(range);
|
||||||
|
const rawRange = {
|
||||||
|
from: range.from,
|
||||||
|
to: range.to,
|
||||||
|
};
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
toRaw: range.to,
|
toRaw: rawRange.to,
|
||||||
fromRaw: range.from,
|
fromRaw: rawRange.from,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
rangeString,
|
rangeString,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
if (onChangeTime) {
|
if (onChangeTime) {
|
||||||
onChangeTime(range);
|
onChangeTime(rawRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
import { Emitter } from 'app/core/core';
|
import { Emitter } from 'app/core/core';
|
||||||
import {
|
import {
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceSelectItem,
|
DataSourceSelectItem,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
QueryFixAction,
|
QueryFixAction,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
TimeRange,
|
||||||
} from '@grafana/ui/src/types';
|
} from '@grafana/ui/src/types';
|
||||||
import {
|
import {
|
||||||
ExploreId,
|
ExploreId,
|
||||||
@@ -89,7 +89,7 @@ export interface InitializeExplorePayload {
|
|||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
eventBridge: Emitter;
|
eventBridge: Emitter;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: TimeRange;
|
||||||
ui: ExploreUIState;
|
ui: ExploreUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import { refreshExplore, testDatasource, loadDatasource } from './actions';
|
import { refreshExplore, testDatasource, loadDatasource } from './actions';
|
||||||
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
|
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
@@ -18,6 +19,7 @@ import { Emitter } from 'app/core/core';
|
|||||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { makeInitialUpdateState } from './reducers';
|
import { makeInitialUpdateState } from './reducers';
|
||||||
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
||||||
|
import { DefaultTimeZone, RawTimeRange } from '@grafana/ui';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||||
getDatasourceSrv: () => ({
|
getDatasourceSrv: () => ({
|
||||||
@@ -29,21 +31,39 @@ jest.mock('app/features/plugins/datasource_srv', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const t = moment.utc();
|
||||||
|
const testRange = {
|
||||||
|
from: t,
|
||||||
|
to: t,
|
||||||
|
raw: {
|
||||||
|
from: t,
|
||||||
|
to: t,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.mock('app/core/utils/explore', () => ({
|
||||||
|
...jest.requireActual('app/core/utils/explore'),
|
||||||
|
getTimeRangeFromUrl: (range: RawTimeRange) => testRange,
|
||||||
|
}));
|
||||||
|
|
||||||
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
||||||
const exploreId = ExploreId.left;
|
const exploreId = ExploreId.left;
|
||||||
const containerWidth = 1920;
|
const containerWidth = 1920;
|
||||||
const eventBridge = {} as Emitter;
|
const eventBridge = {} as Emitter;
|
||||||
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
|
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
|
||||||
const range = { from: 'now', to: 'now' };
|
const timeZone = DefaultTimeZone;
|
||||||
|
const range = testRange;
|
||||||
const urlState: ExploreUrlState = {
|
const urlState: ExploreUrlState = {
|
||||||
datasource: 'some-datasource',
|
datasource: 'some-datasource',
|
||||||
queries: [],
|
queries: [],
|
||||||
range,
|
range: range.raw,
|
||||||
ui,
|
ui,
|
||||||
};
|
};
|
||||||
const updateDefaults = makeInitialUpdateState();
|
const updateDefaults = makeInitialUpdateState();
|
||||||
const update = { ...updateDefaults, ...updateOverides };
|
const update = { ...updateDefaults, ...updateOverides };
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
user: {
|
||||||
|
timeZone,
|
||||||
|
},
|
||||||
explore: {
|
explore: {
|
||||||
[exploreId]: {
|
[exploreId]: {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
@@ -77,7 +97,7 @@ describe('refreshExplore', () => {
|
|||||||
describe('when explore is initialized', () => {
|
describe('when explore is initialized', () => {
|
||||||
describe('and update datasource is set', () => {
|
describe('and update datasource is set', () => {
|
||||||
it('then it should dispatch initializeExplore', async () => {
|
it('then it should dispatch initializeExplore', async () => {
|
||||||
const { exploreId, ui, range, initialState, containerWidth, eventBridge } = setup({ datasource: true });
|
const { exploreId, ui, initialState, containerWidth, eventBridge } = setup({ datasource: true });
|
||||||
|
|
||||||
const dispatchedActions = await thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
@@ -90,7 +110,10 @@ describe('refreshExplore', () => {
|
|||||||
expect(payload.containerWidth).toEqual(containerWidth);
|
expect(payload.containerWidth).toEqual(containerWidth);
|
||||||
expect(payload.eventBridge).toEqual(eventBridge);
|
expect(payload.eventBridge).toEqual(eventBridge);
|
||||||
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
|
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
|
||||||
expect(payload.range).toEqual(range);
|
expect(payload.range.from).toEqual(testRange.from);
|
||||||
|
expect(payload.range.to).toEqual(testRange.to);
|
||||||
|
expect(payload.range.raw.from).toEqual(testRange.raw.from);
|
||||||
|
expect(payload.range.raw.to).toEqual(testRange.raw.to);
|
||||||
expect(payload.ui).toEqual(ui);
|
expect(payload.ui).toEqual(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@@ -16,6 +17,8 @@ import {
|
|||||||
buildQueryTransaction,
|
buildQueryTransaction,
|
||||||
serializeStateToUrlParam,
|
serializeStateToUrlParam,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
|
getTimeRange,
|
||||||
|
getTimeRangeFromUrl,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
@@ -26,12 +29,12 @@ import { ResultGetter } from 'app/types/explore';
|
|||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import {
|
import {
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceSelectItem,
|
DataSourceSelectItem,
|
||||||
QueryHint,
|
QueryHint,
|
||||||
QueryFixAction,
|
QueryFixAction,
|
||||||
|
TimeRange,
|
||||||
} from '@grafana/ui/src/types';
|
} from '@grafana/ui/src/types';
|
||||||
import {
|
import {
|
||||||
ExploreId,
|
ExploreId,
|
||||||
@@ -83,7 +86,7 @@ import {
|
|||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
import { parseTime } from '../TimePicker';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates UI state and save it to the URL
|
* Updates UI state and save it to the URL
|
||||||
@@ -169,8 +172,10 @@ export function changeSize(
|
|||||||
/**
|
/**
|
||||||
* Change the time range of Explore. Usually called from the Time picker or a graph interaction.
|
* Change the time range of Explore. Usually called from the Time picker or a graph interaction.
|
||||||
*/
|
*/
|
||||||
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
|
export function changeTime(exploreId: ExploreId, rawRange: RawTimeRange): ThunkResult<void> {
|
||||||
return dispatch => {
|
return (dispatch, getState) => {
|
||||||
|
const timeZone = getTimeZone(getState().user);
|
||||||
|
const range = getTimeRange(timeZone, rawRange);
|
||||||
dispatch(changeTimeAction({ exploreId, range }));
|
dispatch(changeTimeAction({ exploreId, range }));
|
||||||
dispatch(runQueries(exploreId));
|
dispatch(runQueries(exploreId));
|
||||||
};
|
};
|
||||||
@@ -235,12 +240,14 @@ export function initializeExplore(
|
|||||||
exploreId: ExploreId,
|
exploreId: ExploreId,
|
||||||
datasourceName: string,
|
datasourceName: string,
|
||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
range: RawTimeRange,
|
rawRange: RawTimeRange,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
eventBridge: Emitter,
|
eventBridge: Emitter,
|
||||||
ui: ExploreUIState
|
ui: ExploreUIState
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async (dispatch, getState) => {
|
||||||
|
const timeZone = getTimeZone(getState().user);
|
||||||
|
const range = getTimeRange(timeZone, rawRange);
|
||||||
dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
|
dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
|
||||||
dispatch(
|
dispatch(
|
||||||
initializeExploreAction({
|
initializeExploreAction({
|
||||||
@@ -723,6 +730,23 @@ export function splitOpen(): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toRawTimeRange = (range: TimeRange): RawTimeRange => {
|
||||||
|
let from = range.raw.from;
|
||||||
|
if (moment.isMoment(from)) {
|
||||||
|
from = from.valueOf().toString(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
let to = range.raw.to;
|
||||||
|
if (moment.isMoment(to)) {
|
||||||
|
to = to.valueOf().toString(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves Explore state to URL using the `left` and `right` parameters.
|
* Saves Explore state to URL using the `left` and `right` parameters.
|
||||||
* If split view is not active, `right` will not be set.
|
* If split view is not active, `right` will not be set.
|
||||||
@@ -734,7 +758,7 @@ export function stateSave(): ThunkResult<void> {
|
|||||||
const leftUrlState: ExploreUrlState = {
|
const leftUrlState: ExploreUrlState = {
|
||||||
datasource: left.datasourceInstance.name,
|
datasource: left.datasourceInstance.name,
|
||||||
queries: left.queries.map(clearQueryKeys),
|
queries: left.queries.map(clearQueryKeys),
|
||||||
range: left.range,
|
range: toRawTimeRange(left.range),
|
||||||
ui: {
|
ui: {
|
||||||
showingGraph: left.showingGraph,
|
showingGraph: left.showingGraph,
|
||||||
showingLogs: left.showingLogs,
|
showingLogs: left.showingLogs,
|
||||||
@@ -747,7 +771,7 @@ export function stateSave(): ThunkResult<void> {
|
|||||||
const rightUrlState: ExploreUrlState = {
|
const rightUrlState: ExploreUrlState = {
|
||||||
datasource: right.datasourceInstance.name,
|
datasource: right.datasourceInstance.name,
|
||||||
queries: right.queries.map(clearQueryKeys),
|
queries: right.queries.map(clearQueryKeys),
|
||||||
range: right.range,
|
range: toRawTimeRange(right.range),
|
||||||
ui: {
|
ui: {
|
||||||
showingGraph: right.showingGraph,
|
showingGraph: right.showingGraph,
|
||||||
showingLogs: right.showingLogs,
|
showingLogs: right.showingLogs,
|
||||||
@@ -830,19 +854,20 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { urlState, update, containerWidth, eventBridge } = itemState;
|
const { urlState, update, containerWidth, eventBridge } = itemState;
|
||||||
const { datasource, queries, range, ui } = urlState;
|
const { datasource, queries, range: urlRange, ui } = urlState;
|
||||||
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
|
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
|
||||||
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
const timeZone = getTimeZone(getState().user);
|
||||||
|
const range = getTimeRangeFromUrl(urlRange, timeZone);
|
||||||
|
|
||||||
// need to refresh datasource
|
// need to refresh datasource
|
||||||
if (update.datasource) {
|
if (update.datasource) {
|
||||||
const initialQueries = ensureQueries(queries);
|
const initialQueries = ensureQueries(queries);
|
||||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
dispatch(initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui));
|
||||||
dispatch(initializeExplore(exploreId, datasource, initialQueries, initialRange, containerWidth, eventBridge, ui));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.range) {
|
if (update.range) {
|
||||||
dispatch(changeTimeAction({ exploreId, range: refreshRange as TimeRange }));
|
dispatch(changeTimeAction({ exploreId, range }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to refresh ui state
|
// need to refresh ui state
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
|||||||
initialized: false,
|
initialized: false,
|
||||||
queryTransactions: [],
|
queryTransactions: [],
|
||||||
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
||||||
range: DEFAULT_RANGE,
|
range: {
|
||||||
|
from: null,
|
||||||
|
to: null,
|
||||||
|
raw: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
scanning: false,
|
scanning: false,
|
||||||
scanRange: null,
|
scanRange: null,
|
||||||
showingGraph: true,
|
showingGraph: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import config from 'app/core/config';
|
|||||||
|
|
||||||
export const initialState: UserState = {
|
export const initialState: UserState = {
|
||||||
orgId: config.bootData.user.orgId,
|
orgId: config.bootData.user.orgId,
|
||||||
|
timeZone: config.bootData.user.timezone,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userReducer = (state = initialState, action: any): UserState => {
|
export const userReducer = (state = initialState, action: any): UserState => {
|
||||||
|
|||||||
4
public/app/features/profile/state/selectors.ts
Normal file
4
public/app/features/profile/state/selectors.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { UserState } from 'app/types';
|
||||||
|
import { parseTimeZone } from '@grafana/ui';
|
||||||
|
|
||||||
|
export const getTimeZone = (state: UserState) => parseTimeZone(state.timeZone);
|
||||||
@@ -2,7 +2,6 @@ import { ComponentClass } from 'react';
|
|||||||
import { Value } from 'slate';
|
import { Value } from 'slate';
|
||||||
import {
|
import {
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryResponseData,
|
DataQueryResponseData,
|
||||||
DataSourceSelectItem,
|
DataSourceSelectItem,
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
QueryHint,
|
QueryHint,
|
||||||
ExploreStartPageProps,
|
ExploreStartPageProps,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
TimeRange,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { Emitter, TimeSeries } from 'app/core/core';
|
import { Emitter, TimeSeries } from 'app/core/core';
|
||||||
@@ -189,7 +189,7 @@ export interface ExploreItemState {
|
|||||||
/**
|
/**
|
||||||
* Time range for this Explore. Managed by the time picker and used by all query runs.
|
* Time range for this Explore. Managed by the time picker and used by all query runs.
|
||||||
*/
|
*/
|
||||||
range: TimeRange | RawTimeRange;
|
range: TimeRange;
|
||||||
/**
|
/**
|
||||||
* Scanner function that calculates a new range, triggers a query run, and returns the new range.
|
* Scanner function that calculates a new range, triggers a query run, and returns the new range.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ export interface UsersState {
|
|||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
orgId: number;
|
orgId: number;
|
||||||
|
timeZone: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user