Commit a6c2a8c3 by Torkel Ödegaard

Merge branch 'master' into table-reducer

parents beba9676 eedbc485
...@@ -163,5 +163,5 @@ plugin development. ...@@ -163,5 +163,5 @@ plugin development.
## License ## License
Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE.md). Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE).
...@@ -3,10 +3,10 @@ import React, { PureComponent, CSSProperties, ReactNode } from 'react'; ...@@ -3,10 +3,10 @@ import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
// Utils // Utils
import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils'; import { getColorFromHexRgbOrName, getThresholdForValue } from '../../utils';
// Types // Types
import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types'; import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
const BAR_SIZE_RATIO = 0.8; const BAR_SIZE_RATIO = 0.8;
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import $ from 'jquery'; import $ from 'jquery';
import { Threshold, GrafanaThemeType } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils'; import { getColorFromHexRgbOrName } from '../../utils';
import { Themeable } from '../../index';
import { DisplayValue } from '../../utils/displayValue'; import { DisplayValue, Threshold, GrafanaThemeType, Themeable } from '../../types';
export interface Props extends Themeable { export interface Props extends Themeable {
height: number; height: number;
......
...@@ -76,3 +76,17 @@ export interface TableData { ...@@ -76,3 +76,17 @@ export interface TableData {
rows: any[][]; rows: any[][];
tags?: Tags; tags?: Tags;
} }
export interface AnnotationEvent {
annotation?: any;
dashboardId?: number;
panelId?: number;
userId?: number;
time?: number;
timeEnd?: number;
isRegion?: boolean;
title?: string;
text?: string;
type?: string;
tags?: string;
}
export interface DisplayValue {
text: string; // Show in the UI
numeric: number; // Use isNaN to check if it is a real number
color?: string; // color based on configs or Threshold
title?: string;
}
export interface DecimalInfo {
decimals: number;
scaledDecimals: number;
}
...@@ -6,3 +6,4 @@ export * from './datasource'; ...@@ -6,3 +6,4 @@ export * from './datasource';
export * from './theme'; export * from './theme';
export * from './threshold'; export * from './threshold';
export * from './input'; export * from './input';
export * from './displayValue';
import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, DisplayValue } from './displayValue'; import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, getDecimalsForValue } from './displayValue';
import { MappingType, ValueMapping } from '../types/panel'; import { DisplayValue, MappingType, ValueMapping } from '../types';
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) { function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
processors.forEach(processor => { processors.forEach(processor => {
...@@ -144,6 +144,20 @@ describe('Format value', () => { ...@@ -144,6 +144,20 @@ describe('Format value', () => {
expect(result.text).toEqual('10.0'); expect(result.text).toEqual('10.0');
}); });
it('should set auto decimals, 1 significant', () => {
const value = '1.23';
const instance = getDisplayProcessor({ decimals: null });
expect(instance(value).text).toEqual('1.2');
});
it('should set auto decimals, 2 significant', () => {
const value = '0.0245';
const instance = getDisplayProcessor({ decimals: null });
expect(instance(value).text).toEqual('0.02');
});
it('should return mapped value if there are matching value mappings', () => { it('should return mapped value if there are matching value mappings', () => {
const valueMappings: ValueMapping[] = [ const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
...@@ -155,3 +169,18 @@ describe('Format value', () => { ...@@ -155,3 +169,18 @@ describe('Format value', () => {
expect(instance(value).text).toEqual('1-20'); expect(instance(value).text).toEqual('1-20');
}); });
}); });
describe('getDecimalsForValue()', () => {
it('should calculate reasonable decimals precision for given value', () => {
expect(getDecimalsForValue(1.01)).toEqual({ decimals: 1, scaledDecimals: 4 });
expect(getDecimalsForValue(9.01)).toEqual({ decimals: 0, scaledDecimals: 2 });
expect(getDecimalsForValue(1.1)).toEqual({ decimals: 1, scaledDecimals: 4 });
expect(getDecimalsForValue(2)).toEqual({ decimals: 0, scaledDecimals: 2 });
expect(getDecimalsForValue(20)).toEqual({ decimals: 0, scaledDecimals: 1 });
expect(getDecimalsForValue(200)).toEqual({ decimals: 0, scaledDecimals: 0 });
expect(getDecimalsForValue(2000)).toEqual({ decimals: 0, scaledDecimals: 0 });
expect(getDecimalsForValue(20000)).toEqual({ decimals: 0, scaledDecimals: -2 });
expect(getDecimalsForValue(200000)).toEqual({ decimals: 0, scaledDecimals: -3 });
expect(getDecimalsForValue(200000000)).toEqual({ decimals: 0, scaledDecimals: -6 });
});
});
import { ValueMapping, Threshold } from '../types'; // Libraries
import _ from 'lodash'; import _ from 'lodash';
import { getValueFormat, DecimalCount } from './valueFormats/valueFormats'; import moment from 'moment';
// Utils
import { getValueFormat } from './valueFormats/valueFormats';
import { getMappedValue } from './valueMappings'; import { getMappedValue } from './valueMappings';
import { GrafanaTheme, GrafanaThemeType } from '../types';
import { getColorFromHexRgbOrName } from './namedColorsPalette'; import { getColorFromHexRgbOrName } from './namedColorsPalette';
import moment from 'moment';
export interface DisplayValue { // Types
text: string; // Show in the UI import { Threshold, ValueMapping, DecimalInfo, DisplayValue, GrafanaTheme, GrafanaThemeType } from '../types';
numeric: number; // Use isNaN to check if it is a real number import { DecimalCount } from './valueFormats/valueFormats';
color?: string; // color based on configs or Threshold
} export type DisplayProcessor = (value: any) => DisplayValue;
export interface DisplayValueOptions { export interface DisplayValueOptions {
unit?: string; unit?: string;
decimals?: DecimalCount; decimals?: DecimalCount;
scaledDecimals?: DecimalCount;
dateFormat?: string; // If set try to convert numbers to date dateFormat?: string; // If set try to convert numbers to date
color?: string; color?: string;
...@@ -32,11 +32,10 @@ export interface DisplayValueOptions { ...@@ -32,11 +32,10 @@ export interface DisplayValueOptions {
theme?: GrafanaTheme; // Will pick 'dark' if not defined theme?: GrafanaTheme; // Will pick 'dark' if not defined
} }
export type DisplayProcessor = (value: any) => DisplayValue;
export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor { export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor {
if (options && !_.isEmpty(options)) { if (options && !_.isEmpty(options)) {
const formatFunc = getValueFormat(options.unit || 'none'); const formatFunc = getValueFormat(options.unit || 'none');
return (value: any) => { return (value: any) => {
const { prefix, suffix, mappings, thresholds, theme } = options; const { prefix, suffix, mappings, thresholds, theme } = options;
let color = options.color; let color = options.color;
...@@ -47,12 +46,15 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce ...@@ -47,12 +46,15 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
let shouldFormat = true; let shouldFormat = true;
if (mappings && mappings.length > 0) { if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, value); const mappedValue = getMappedValue(mappings, value);
if (mappedValue) { if (mappedValue) {
text = mappedValue.text; text = mappedValue.text;
const v = toNumber(text); const v = toNumber(text);
if (!isNaN(v)) { if (!isNaN(v)) {
numeric = v; numeric = v;
} }
shouldFormat = false; shouldFormat = false;
} }
} }
...@@ -67,7 +69,19 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce ...@@ -67,7 +69,19 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
if (!isNaN(numeric)) { if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) { if (shouldFormat && !_.isBoolean(value)) {
text = formatFunc(numeric, options.decimals, options.scaledDecimals, options.isUtc); let decimals;
let scaledDecimals = 0;
if (!options.decimals) {
const decimalInfo = getDecimalsForValue(value);
decimals = decimalInfo.decimals;
scaledDecimals = decimalInfo.scaledDecimals;
} else {
decimals = options.decimals;
}
text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
} }
if (thresholds && thresholds.length > 0) { if (thresholds && thresholds.length > 0) {
color = getColorFromThreshold(numeric, thresholds, theme); color = getColorFromThreshold(numeric, thresholds, theme);
...@@ -143,3 +157,39 @@ export function getColorFromThreshold(value: number, thresholds: Threshold[], th ...@@ -143,3 +157,39 @@ export function getColorFromThreshold(value: number, thresholds: Threshold[], th
// Use the first threshold as the default color // Use the first threshold as the default color
return getColorFromHexRgbOrName(thresholds[0].color, themeType); return getColorFromHexRgbOrName(thresholds[0].color, themeType);
} }
export function getDecimalsForValue(value: number): DecimalInfo {
const delta = value / 2;
let dec = -Math.floor(Math.log(delta) / Math.LN10);
const magn = Math.pow(10, -dec);
const norm = delta / magn; // norm is between 1.0 and 10.0
let size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
// reduce starting decimals if not needed
if (Math.floor(value) === value) {
dec = 0;
}
const decimals = Math.max(0, dec);
const scaledDecimals = decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return { decimals, scaledDecimals };
}
import { LocationUpdate } from 'app/types'; import { LocationUpdate } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux';
export enum CoreActionTypes { export const updateLocation = actionCreatorFactory<LocationUpdate>('UPDATE_LOCATION').create();
UpdateLocation = 'UPDATE_LOCATION',
}
export type Action = UpdateLocationAction;
export interface UpdateLocationAction {
type: CoreActionTypes.UpdateLocation;
payload: LocationUpdate;
}
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
type: CoreActionTypes.UpdateLocation,
payload: location,
});
import { Action, CoreActionTypes } from 'app/core/actions/location';
import { LocationState } from 'app/types'; import { LocationState } from 'app/types';
import { renderUrl } from 'app/core/utils/url'; import { renderUrl } from 'app/core/utils/url';
import _ from 'lodash'; import _ from 'lodash';
import { reducerFactory } from 'app/core/redux';
import { updateLocation } from 'app/core/actions';
export const initialState: LocationState = { export const initialState: LocationState = {
url: '', url: '',
...@@ -12,9 +13,10 @@ export const initialState: LocationState = { ...@@ -12,9 +13,10 @@ export const initialState: LocationState = {
lastUpdated: 0, lastUpdated: 0,
}; };
export const locationReducer = (state = initialState, action: Action): LocationState => { export const locationReducer = reducerFactory<LocationState>(initialState)
switch (action.type) { .addMapper({
case CoreActionTypes.UpdateLocation: { filter: updateLocation,
mapper: (state, action): LocationState => {
const { path, routeParams, replace } = action.payload; const { path, routeParams, replace } = action.payload;
let query = action.payload.query || state.query; let query = action.payload.query || state.query;
...@@ -31,8 +33,6 @@ export const locationReducer = (state = initialState, action: Action): LocationS ...@@ -31,8 +33,6 @@ export const locationReducer = (state = initialState, action: Action): LocationS
replace: replace === true, replace: replace === true,
lastUpdated: new Date().getTime(), lastUpdated: new Date().getTime(),
}; };
} },
} })
.create();
return state;
};
...@@ -68,5 +68,9 @@ export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator): ...@@ -68,5 +68,9 @@ export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator):
return mock; return mock;
}; };
export const mockActionCreator = (creator: ActionCreator<any>) => {
return Object.assign(jest.fn(), creator);
};
// Should only be used by tests // Should only be used by tests
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0); export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
...@@ -3,6 +3,8 @@ import { shallow } from 'enzyme'; ...@@ -3,6 +3,8 @@ import { shallow } from 'enzyme';
import { AlertRuleList, Props } from './AlertRuleList'; import { AlertRuleList, Props } from './AlertRuleList';
import { AlertRule, NavModel } from '../../types'; import { AlertRule, NavModel } from '../../types';
import appEvents from '../../core/app_events'; import appEvents from '../../core/app_events';
import { mockActionCreator } from 'app/core/redux';
import { updateLocation } from 'app/core/actions';
jest.mock('../../core/app_events', () => ({ jest.mock('../../core/app_events', () => ({
emit: jest.fn(), emit: jest.fn(),
...@@ -12,7 +14,7 @@ const setup = (propOverrides?: object) => { ...@@ -12,7 +14,7 @@ const setup = (propOverrides?: object) => {
const props: Props = { const props: Props = {
navModel: {} as NavModel, navModel: {} as NavModel,
alertRules: [] as AlertRule[], alertRules: [] as AlertRule[],
updateLocation: jest.fn(), updateLocation: mockActionCreator(updateLocation),
getAlertRulesAsync: jest.fn(), getAlertRulesAsync: jest.fn(),
setSearchQuery: jest.fn(), setSearchQuery: jest.fn(),
togglePauseAlertRule: jest.fn(), togglePauseAlertRule: jest.fn(),
......
import { AnnotationsSrv } from './annotations_srv'; import { AnnotationsSrv } from './annotations_srv';
import { eventEditor } from './event_editor'; import { eventEditor } from './event_editor';
import { EventManager } from './event_manager'; import { EventManager } from './event_manager';
import { AnnotationEvent } from './event';
import { annotationTooltipDirective } from './annotation_tooltip'; import { annotationTooltipDirective } from './annotation_tooltip';
export { AnnotationsSrv, eventEditor, EventManager, annotationTooltipDirective };
export { AnnotationsSrv, eventEditor, EventManager, AnnotationEvent, annotationTooltipDirective };
export class AnnotationEvent {
dashboardId: number;
panelId: number;
userId: number;
time: any;
timeEnd: any;
isRegion: boolean;
text: string;
type: string;
tags: string;
}
...@@ -2,7 +2,7 @@ import _ from 'lodash'; ...@@ -2,7 +2,7 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { coreModule } from 'app/core/core'; import { coreModule } from 'app/core/core';
import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { AnnotationEvent } from './event'; import { AnnotationEvent } from '@grafana/ui';
export class EventEditorCtrl { export class EventEditorCtrl {
panelCtrl: MetricsPanelCtrl; panelCtrl: MetricsPanelCtrl;
......
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { import {
OK_COLOR, OK_COLOR,
...@@ -11,7 +10,7 @@ import { ...@@ -11,7 +10,7 @@ import {
} from '@grafana/ui'; } from '@grafana/ui';
import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { AnnotationEvent } from './event'; import { AnnotationEvent } from '@grafana/ui';
export class EventManager { export class EventManager {
event: AnnotationEvent; event: AnnotationEvent;
...@@ -31,16 +30,17 @@ export class EventManager { ...@@ -31,16 +30,17 @@ export class EventManager {
updateTime(range) { updateTime(range) {
if (!this.event) { if (!this.event) {
this.event = new AnnotationEvent(); this.event = {};
this.event.dashboardId = this.panelCtrl.dashboard.id; this.event.dashboardId = this.panelCtrl.dashboard.id;
this.event.panelId = this.panelCtrl.panel.id; this.event.panelId = this.panelCtrl.panel.id;
} }
// update time // update time
this.event.time = moment(range.from); this.event.time = range.from;
this.event.isRegion = false; this.event.isRegion = false;
if (range.to) { if (range.to) {
this.event.timeEnd = moment(range.to); this.event.timeEnd = range.to;
this.event.isRegion = true; this.event.isRegion = true;
} }
...@@ -90,8 +90,8 @@ export class EventManager { ...@@ -90,8 +90,8 @@ export class EventManager {
annotations = [ annotations = [
{ {
isRegion: true, isRegion: true,
min: this.event.time.valueOf(), min: this.event.time,
timeEnd: this.event.timeEnd.valueOf(), timeEnd: this.event.timeEnd,
text: this.event.text, text: this.event.text,
eventType: '$__editing', eventType: '$__editing',
editModel: this.event, editModel: this.event,
...@@ -100,7 +100,7 @@ export class EventManager { ...@@ -100,7 +100,7 @@ export class EventManager {
} else { } else {
annotations = [ annotations = [
{ {
min: this.event.time.valueOf(), min: this.event.time,
text: this.event.text, text: this.event.text,
editModel: this.event, editModel: this.event,
eventType: '$__editing', eventType: '$__editing',
......
...@@ -3,8 +3,9 @@ import { shallow, ShallowWrapper } from 'enzyme'; ...@@ -3,8 +3,9 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage'; import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
import { cleanUpDashboard } from '../state/actions'; import { cleanUpDashboard } from '../state/actions';
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux'; import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock, mockActionCreator } from 'app/core/redux';
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types'; import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
import { updateLocation } from 'app/core/actions';
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({})); jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
...@@ -62,7 +63,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) = ...@@ -62,7 +63,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
initPhase: DashboardInitPhase.NotStarted, initPhase: DashboardInitPhase.NotStarted,
isInitSlow: false, isInitSlow: false,
initDashboard: jest.fn(), initDashboard: jest.fn(),
updateLocation: jest.fn(), updateLocation: mockActionCreator(updateLocation),
notifyApp: jest.fn(), notifyApp: jest.fn(),
cleanUpDashboard: ctx.cleanUpDashboardMock, cleanUpDashboard: ctx.cleanUpDashboardMock,
dashboard: null, dashboard: null,
......
...@@ -4,10 +4,9 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; ...@@ -4,10 +4,9 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector'; import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
import { UpdateLocationAction } from 'app/core/actions/location';
import { buildNavModel } from './navModel'; import { buildNavModel } from './navModel';
import { DataSourceSettings } from '@grafana/ui/src/types'; import { DataSourceSettings } from '@grafana/ui/src/types';
import { Plugin, StoreState } from 'app/types'; import { Plugin, StoreState, LocationUpdate } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux'; import { actionCreatorFactory } from 'app/core/redux';
import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory'; import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
...@@ -32,12 +31,12 @@ export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_N ...@@ -32,12 +31,12 @@ export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_N
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create(); export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
export type Action = export type Action =
| UpdateLocationAction
| UpdateNavIndexAction | UpdateNavIndexAction
| ActionOf<DataSourceSettings> | ActionOf<DataSourceSettings>
| ActionOf<DataSourceSettings[]> | ActionOf<DataSourceSettings[]>
| ActionOf<Plugin> | ActionOf<Plugin>
| ActionOf<Plugin[]>; | ActionOf<Plugin[]>
| ActionOf<LocationUpdate>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>; type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
......
// Libraries // Libraries
import React, { ComponentClass } from 'react'; import React, { ComponentClass } from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
// @ts-ignore
import { connect } from 'react-redux'; import { connect } from 'react-redux';
// @ts-ignore
import _ from 'lodash'; import _ from 'lodash';
import { AutoSizer } from 'react-virtualized'; import { AutoSizer } from 'react-virtualized';
...@@ -18,11 +20,19 @@ import TableContainer from './TableContainer'; ...@@ -18,11 +20,19 @@ import TableContainer from './TableContainer';
import TimePicker, { parseTime } from './TimePicker'; import TimePicker, { parseTime } from './TimePicker';
// Actions // Actions
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; import {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
scanStart,
setQueries,
refreshExplore,
} from './state/actions';
// Types // Types
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui'; import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId, ExploreUpdateState } 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 } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
...@@ -42,6 +52,8 @@ interface ExploreProps { ...@@ -42,6 +52,8 @@ interface ExploreProps {
initialized: boolean; initialized: boolean;
modifyQueries: typeof modifyQueries; modifyQueries: typeof modifyQueries;
range: RawTimeRange; range: RawTimeRange;
update: ExploreUpdateState;
refreshExplore: typeof refreshExplore;
scanner?: RangeScanner; scanner?: RangeScanner;
scanning?: boolean; scanning?: boolean;
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
...@@ -53,8 +65,8 @@ interface ExploreProps { ...@@ -53,8 +65,8 @@ interface ExploreProps {
supportsGraph: boolean | null; supportsGraph: boolean | null;
supportsLogs: boolean | null; supportsLogs: boolean | null;
supportsTable: boolean | null; supportsTable: boolean | null;
urlState: ExploreUrlState;
queryKeys: string[]; queryKeys: string[];
urlState: ExploreUrlState;
} }
/** /**
...@@ -89,23 +101,22 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -89,23 +101,22 @@ export class Explore extends React.PureComponent<ExploreProps> {
*/ */
timepickerRef: React.RefObject<TimePicker>; timepickerRef: React.RefObject<TimePicker>;
constructor(props) { constructor(props: ExploreProps) {
super(props); super(props);
this.exploreEvents = new Emitter(); this.exploreEvents = new Emitter();
this.timepickerRef = React.createRef(); this.timepickerRef = React.createRef();
} }
async componentDidMount() { componentDidMount() {
const { exploreId, initialized, urlState } = this.props; const { exploreId, urlState, initialized } = this.props;
// Don't initialize on split, but need to initialize urlparameters when present const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
if (!initialized) { const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
// Load URL state and parse range const initialQueries: DataQuery[] = ensureQueries(queries);
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState; const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); const width = this.el ? this.el.offsetWidth : 0;
const initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
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
if (!initialized) {
this.props.initializeExplore( this.props.initializeExplore(
exploreId, exploreId,
initialDatasource, initialDatasource,
...@@ -122,7 +133,11 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -122,7 +133,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.exploreEvents.removeAllListeners(); this.exploreEvents.removeAllListeners();
} }
getRef = el => { componentDidUpdate(prevProps: ExploreProps) {
this.refreshExplore();
}
getRef = (el: any) => {
this.el = el; this.el = el;
}; };
...@@ -142,7 +157,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -142,7 +157,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.onModifyQueries({ type: 'ADD_FILTER', key, value }); this.onModifyQueries({ type: 'ADD_FILTER', key, value });
}; };
onModifyQueries = (action, index?: number) => { onModifyQueries = (action: any, index?: number) => {
const { datasourceInstance } = this.props; const { datasourceInstance } = this.props;
if (datasourceInstance && datasourceInstance.modifyQuery) { if (datasourceInstance && datasourceInstance.modifyQuery) {
const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification); const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification);
...@@ -169,6 +184,14 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -169,6 +184,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.props.scanStopAction({ exploreId: this.props.exploreId }); this.props.scanStopAction({ exploreId: this.props.exploreId });
}; };
refreshExplore = () => {
const { exploreId, update } = this.props;
if (update.queries || update.ui || update.range || update.datasource) {
this.props.refreshExplore(exploreId);
}
};
render() { render() {
const { const {
StartPage, StartPage,
...@@ -241,7 +264,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -241,7 +264,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
} }
} }
function mapStateToProps(state: StoreState, { exploreId }) { 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];
...@@ -258,6 +281,8 @@ function mapStateToProps(state: StoreState, { exploreId }) { ...@@ -258,6 +281,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
supportsLogs, supportsLogs,
supportsTable, supportsTable,
queryKeys, queryKeys,
urlState,
update,
} = item; } = item;
return { return {
StartPage, StartPage,
...@@ -273,6 +298,8 @@ function mapStateToProps(state: StoreState, { exploreId }) { ...@@ -273,6 +298,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
supportsLogs, supportsLogs,
supportsTable, supportsTable,
queryKeys, queryKeys,
urlState,
update,
}; };
} }
...@@ -281,6 +308,7 @@ const mapDispatchToProps = { ...@@ -281,6 +308,7 @@ const mapDispatchToProps = {
changeTime, changeTime,
initializeExplore, initializeExplore,
modifyQueries, modifyQueries,
refreshExplore,
scanStart, scanStart,
scanStopAction, scanStopAction,
setQueries, setQueries,
......
...@@ -2,65 +2,37 @@ import React, { Component } from 'react'; ...@@ -2,65 +2,37 @@ import React, { Component } 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 { updateLocation } from 'app/core/actions';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import ErrorBoundary from './ErrorBoundary'; import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore'; import Explore from './Explore';
import { CustomScrollbar } from '@grafana/ui'; import { CustomScrollbar } from '@grafana/ui';
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes'; import { resetExploreAction } from './state/actionTypes';
interface WrapperProps { interface WrapperProps {
initializeExploreSplitAction: typeof initializeExploreSplitAction;
split: boolean; split: boolean;
updateLocation: typeof updateLocation;
resetExploreAction: typeof resetExploreAction; resetExploreAction: typeof resetExploreAction;
urlStates: { [key: string]: string };
} }
export class Wrapper extends Component<WrapperProps> { export class Wrapper extends Component<WrapperProps> {
initialSplit: boolean;
urlStates: { [key: string]: ExploreUrlState };
constructor(props: WrapperProps) {
super(props);
this.urlStates = {};
const { left, right } = props.urlStates;
if (props.urlStates.left) {
this.urlStates.leftState = parseUrlState(left);
}
if (props.urlStates.right) {
this.urlStates.rightState = parseUrlState(right);
this.initialSplit = true;
}
}
componentDidMount() {
if (this.initialSplit) {
this.props.initializeExploreSplitAction();
}
}
componentWillUnmount() { componentWillUnmount() {
this.props.resetExploreAction(); this.props.resetExploreAction();
} }
render() { render() {
const { split } = this.props; const { split } = this.props;
const { leftState, rightState } = this.urlStates;
return ( return (
<div className="page-scrollbar-wrapper"> <div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page"> <CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
<div className="explore-wrapper"> <div className="explore-wrapper">
<ErrorBoundary> <ErrorBoundary>
<Explore exploreId={ExploreId.left} urlState={leftState} /> <Explore exploreId={ExploreId.left} />
</ErrorBoundary> </ErrorBoundary>
{split && ( {split && (
<ErrorBoundary> <ErrorBoundary>
<Explore exploreId={ExploreId.right} urlState={rightState} /> <Explore exploreId={ExploreId.right} />
</ErrorBoundary> </ErrorBoundary>
)} )}
</div> </div>
...@@ -71,14 +43,11 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -71,14 +43,11 @@ export class Wrapper extends Component<WrapperProps> {
} }
const mapStateToProps = (state: StoreState) => { const mapStateToProps = (state: StoreState) => {
const urlStates = state.location.query;
const { split } = state.explore; const { split } = state.explore;
return { split, urlStates }; return { split };
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
initializeExploreSplitAction,
updateLocation,
resetExploreAction, resetExploreAction,
}; };
......
...@@ -24,17 +24,11 @@ import { LogLevel } from 'app/core/logs_model'; ...@@ -24,17 +24,11 @@ import { LogLevel } from 'app/core/logs_model';
* *
*/ */
export enum ActionTypes { export enum ActionTypes {
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
SplitClose = 'explore/SPLIT_CLOSE', SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN', SplitOpen = 'explore/SPLIT_OPEN',
ResetExplore = 'explore/RESET_EXPLORE', ResetExplore = 'explore/RESET_EXPLORE',
} }
export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit;
payload: {};
}
export interface SplitCloseAction { export interface SplitCloseAction {
type: ActionTypes.SplitClose; type: ActionTypes.SplitClose;
payload: {}; payload: {};
...@@ -154,10 +148,6 @@ export interface RemoveQueryRowPayload { ...@@ -154,10 +148,6 @@ export interface RemoveQueryRowPayload {
index: number; index: number;
} }
export interface RunQueriesEmptyPayload {
exploreId: ExploreId;
}
export interface ScanStartPayload { export interface ScanStartPayload {
exploreId: ExploreId; exploreId: ExploreId;
scanner: RangeScanner; scanner: RangeScanner;
...@@ -260,11 +250,6 @@ export const initializeExploreAction = actionCreatorFactory<InitializeExplorePay ...@@ -260,11 +250,6 @@ export const initializeExploreAction = actionCreatorFactory<InitializeExplorePay
).create(); ).create();
/** /**
* Initialize the wrapper split state
*/
export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create();
/**
* Display an error that happened during the selection of a datasource * Display an error that happened during the selection of a datasource
*/ */
export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>( export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
...@@ -342,7 +327,6 @@ export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransacti ...@@ -342,7 +327,6 @@ export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransacti
*/ */
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create(); export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create(); export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create();
export const runQueriesEmptyAction = actionCreatorFactory<RunQueriesEmptyPayload>('explore/RUN_QUERIES_EMPTY').create();
/** /**
* Start a scan for more results using the given scanner. * Start a scan for more results using the given scanner.
...@@ -411,12 +395,7 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>( ...@@ -411,12 +395,7 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create(); export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create(); export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
export type HigherOrderAction = export type HigherOrderAction = SplitCloseAction | SplitOpenAction | ResetExploreAction | ActionOf<any>;
| InitializeExploreSplitAction
| SplitCloseAction
| SplitOpenAction
| ResetExploreAction
| ActionOf<any>;
export type Action = export type Action =
| ActionOf<AddQueryRowPayload> | ActionOf<AddQueryRowPayload>
...@@ -435,7 +414,6 @@ export type Action = ...@@ -435,7 +414,6 @@ export type Action =
| ActionOf<QueryTransactionStartPayload> | ActionOf<QueryTransactionStartPayload>
| ActionOf<QueryTransactionSuccessPayload> | ActionOf<QueryTransactionSuccessPayload>
| ActionOf<RemoveQueryRowPayload> | ActionOf<RemoveQueryRowPayload>
| ActionOf<RunQueriesEmptyPayload>
| ActionOf<ScanStartPayload> | ActionOf<ScanStartPayload>
| ActionOf<ScanRangePayload> | ActionOf<ScanRangePayload>
| ActionOf<SetQueriesPayload> | ActionOf<SetQueriesPayload>
......
import { refreshExplore } from './actions';
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { LogsDedupStrategy } from 'app/core/logs_model';
import {
initializeExploreAction,
InitializeExplorePayload,
changeTimeAction,
updateUIStateAction,
setQueriesAction,
} from './actionTypes';
import { Emitter } from 'app/core/core';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { makeInitialUpdateState } from './reducers';
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
getExternal: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue({
testDatasource: jest.fn(),
init: jest.fn(),
}),
}),
}));
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
const exploreId = ExploreId.left;
const containerWidth = 1920;
const eventBridge = {} as Emitter;
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
const range = { from: 'now', to: 'now' };
const urlState: ExploreUrlState = { datasource: 'some-datasource', queries: [], range, ui };
const updateDefaults = makeInitialUpdateState();
const update = { ...updateDefaults, ...updateOverides };
const initialState = {
explore: {
[exploreId]: {
initialized: true,
urlState,
containerWidth,
eventBridge,
update,
datasourceInstance: { name: 'some-datasource' },
queries: [],
range,
ui,
},
},
};
return {
initialState,
exploreId,
range,
ui,
containerWidth,
eventBridge,
};
};
describe('refreshExplore', () => {
describe('when explore is initialized', () => {
describe('and update datasource is set', () => {
it('then it should dispatch initializeExplore', () => {
const { exploreId, ui, range, initialState, containerWidth, eventBridge } = setup({ datasource: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
const initializeExplore = dispatchedActions[0] as ActionOf<InitializeExplorePayload>;
const { type, payload } = initializeExplore;
expect(type).toEqual(initializeExploreAction.type);
expect(payload.containerWidth).toEqual(containerWidth);
expect(payload.eventBridge).toEqual(eventBridge);
expect(payload.exploreDatasources).toEqual([]);
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
expect(payload.range).toEqual(range);
expect(payload.ui).toEqual(ui);
return true;
});
});
});
describe('and update range is set', () => {
it('then it should dispatch changeTimeAction', () => {
const { exploreId, range, initialState } = setup({ range: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(changeTimeAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, range });
return true;
});
});
});
describe('and update ui is set', () => {
it('then it should dispatch updateUIStateAction', () => {
const { exploreId, initialState, ui } = setup({ ui: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(updateUIStateAction.type);
expect(dispatchedActions[0].payload).toEqual({ ...ui, exploreId });
return true;
});
});
});
describe('and update queries is set', () => {
it('then it should dispatch setQueriesAction', () => {
const { exploreId, initialState } = setup({ queries: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
return true;
});
});
});
});
describe('when update is not initialized', () => {
it('then it should not dispatch any actions', () => {
const exploreId = ExploreId.left;
const initialState = { explore: { [exploreId]: { initialized: false } } };
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenThereAreNoDispatchedActions();
});
});
});
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
updateHistory, updateHistory,
buildQueryTransaction, buildQueryTransaction,
serializeStateToUrlParam, serializeStateToUrlParam,
parseUrlState,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
// Actions // Actions
...@@ -54,7 +55,6 @@ import { ...@@ -54,7 +55,6 @@ import {
queryTransactionStartAction, queryTransactionStartAction,
queryTransactionSuccessAction, queryTransactionSuccessAction,
scanRangeAction, scanRangeAction,
runQueriesEmptyAction,
scanStartAction, scanStartAction,
setQueriesAction, setQueriesAction,
splitCloseAction, splitCloseAction,
...@@ -67,9 +67,11 @@ import { ...@@ -67,9 +67,11 @@ import {
ToggleLogsPayload, ToggleLogsPayload,
ToggleTablePayload, ToggleTablePayload,
updateUIStateAction, updateUIStateAction,
runQueriesAction,
} 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';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>; type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
...@@ -518,7 +520,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { ...@@ -518,7 +520,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
} = getState().explore[exploreId]; } = getState().explore[exploreId];
if (!hasNonEmptyQuery(queries)) { if (!hasNonEmptyQuery(queries)) {
dispatch(runQueriesEmptyAction({ exploreId })); dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location dispatch(stateSave()); // Remember to saves to state and update location
return; return;
} }
...@@ -527,6 +529,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { ...@@ -527,6 +529,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
// but we're using the datasource interval limit for now // but we're using the datasource interval limit for now
const interval = datasourceInstance.interval; const interval = datasourceInstance.interval;
dispatch(runQueriesAction());
// Keep table queries first since they need to return quickly // Keep table queries first since they need to return quickly
if ((ignoreUIState || showingTable) && supportsTable) { if ((ignoreUIState || showingTable) && supportsTable) {
dispatch( dispatch(
...@@ -657,11 +660,15 @@ export function splitClose(): ThunkResult<void> { ...@@ -657,11 +660,15 @@ export function splitClose(): ThunkResult<void> {
export function splitOpen(): ThunkResult<void> { export function splitOpen(): ThunkResult<void> {
return (dispatch, getState) => { return (dispatch, getState) => {
// Clone left state to become the right state // Clone left state to become the right state
const leftState = getState().explore.left; const leftState = getState().explore[ExploreId.left];
const queryState = getState().location.query[ExploreId.left] as string;
const urlState = parseUrlState(queryState);
const itemState = { const itemState = {
...leftState, ...leftState,
queryTransactions: [], queryTransactions: [],
queries: leftState.queries.slice(), queries: leftState.queries.slice(),
exploreId: ExploreId.right,
urlState,
}; };
dispatch(splitOpenAction({ itemState })); dispatch(splitOpenAction({ itemState }));
dispatch(stateSave()); dispatch(stateSave());
...@@ -766,3 +773,44 @@ export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy) ...@@ -766,3 +773,44 @@ export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy)
dispatch(updateExploreUIState(exploreId, { dedupStrategy })); dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
}; };
}; };
export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const itemState = getState().explore[exploreId];
if (!itemState.initialized) {
return;
}
const { urlState, update, containerWidth, eventBridge } = itemState;
const { datasource, queries, range, ui } = urlState;
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
// need to refresh datasource
if (update.datasource) {
const initialQueries = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
dispatch(initializeExplore(exploreId, datasource, initialQueries, initialRange, containerWidth, eventBridge, ui));
return;
}
if (update.range) {
dispatch(changeTimeAction({ exploreId, range: refreshRange as TimeRange }));
}
// need to refresh ui state
if (update.ui) {
dispatch(updateUIStateAction({ ...ui, exploreId }));
}
// need to refresh queries
if (update.queries) {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
}
// always run queries when refresh is needed
if (update.queries || update.ui || update.range) {
dispatch(runQueries(exploreId));
}
};
}
// @ts-ignore
import _ from 'lodash';
import { import {
calculateResultsFromQueryTransactions, calculateResultsFromQueryTransactions,
generateEmptyQuery, generateEmptyQuery,
getIntervals, getIntervals,
ensureQueries, ensureQueries,
getQueryKeys, getQueryKeys,
parseUrlState,
DEFAULT_UI_STATE,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types'; import { DataQuery } from '@grafana/ui/src/types';
import { HigherOrderAction, ActionTypes } from './actionTypes'; import { HigherOrderAction, ActionTypes } from './actionTypes';
...@@ -28,7 +32,6 @@ import { ...@@ -28,7 +32,6 @@ import {
queryTransactionStartAction, queryTransactionStartAction,
queryTransactionSuccessAction, queryTransactionSuccessAction,
removeQueryRowAction, removeQueryRowAction,
runQueriesEmptyAction,
scanRangeAction, scanRangeAction,
scanStartAction, scanStartAction,
scanStopAction, scanStopAction,
...@@ -40,6 +43,8 @@ import { ...@@ -40,6 +43,8 @@ import {
updateUIStateAction, updateUIStateAction,
toggleLogLevelAction, toggleLogLevelAction,
} from './actionTypes'; } from './actionTypes';
import { updateLocation } from 'app/core/actions/location';
import { LocationUpdate } from 'app/types';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
...@@ -49,6 +54,12 @@ export const DEFAULT_RANGE = { ...@@ -49,6 +54,12 @@ export const DEFAULT_RANGE = {
// Millies step for helper bar charts // Millies step for helper bar charts
const DEFAULT_GRAPH_INTERVAL = 15 * 1000; const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
export const makeInitialUpdateState = (): ExploreUpdateState => ({
datasource: false,
queries: false,
range: false,
ui: false,
});
/** /**
* Returns a fresh Explore area state * Returns a fresh Explore area state
*/ */
...@@ -76,6 +87,8 @@ export const makeExploreItemState = (): ExploreItemState => ({ ...@@ -76,6 +87,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
supportsLogs: null, supportsLogs: null,
supportsTable: null, supportsTable: null,
queryKeys: [], queryKeys: [],
urlState: null,
update: makeInitialUpdateState(),
}); });
/** /**
...@@ -195,6 +208,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -195,6 +208,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
initialized: true, initialized: true,
queryKeys: getQueryKeys(queries, state.datasourceInstance), queryKeys: getQueryKeys(queries, state.datasourceInstance),
...ui, ...ui,
update: makeInitialUpdateState(),
}; };
}, },
}) })
...@@ -208,13 +222,23 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -208,13 +222,23 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
.addMapper({ .addMapper({
filter: loadDatasourceFailureAction, filter: loadDatasourceFailureAction,
mapper: (state, action): ExploreItemState => { mapper: (state, action): ExploreItemState => {
return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; return {
...state,
datasourceError: action.payload.error,
datasourceLoading: false,
update: makeInitialUpdateState(),
};
}, },
}) })
.addMapper({ .addMapper({
filter: loadDatasourceMissingAction, filter: loadDatasourceMissingAction,
mapper: (state): ExploreItemState => { mapper: (state): ExploreItemState => {
return { ...state, datasourceMissing: true, datasourceLoading: false }; return {
...state,
datasourceMissing: true,
datasourceLoading: false,
update: makeInitialUpdateState(),
};
}, },
}) })
.addMapper({ .addMapper({
...@@ -253,6 +277,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -253,6 +277,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
datasourceError: null, datasourceError: null,
logsHighlighterExpressions: undefined, logsHighlighterExpressions: undefined,
queryTransactions: [], queryTransactions: [],
update: makeInitialUpdateState(),
}; };
}, },
}) })
...@@ -262,7 +287,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -262,7 +287,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
const { queries, queryTransactions } = state; const { queries, queryTransactions } = state;
const { modification, index, modifier } = action.payload; const { modification, index, modifier } = action.payload;
let nextQueries: DataQuery[]; let nextQueries: DataQuery[];
let nextQueryTransactions; let nextQueryTransactions: QueryTransaction[];
if (index === undefined) { if (index === undefined) {
// Modify all queries // Modify all queries
nextQueries = queries.map((query, i) => ({ nextQueries = queries.map((query, i) => ({
...@@ -303,7 +328,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -303,7 +328,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
filter: queryTransactionFailureAction, filter: queryTransactionFailureAction,
mapper: (state, action): ExploreItemState => { mapper: (state, action): ExploreItemState => {
const { queryTransactions } = action.payload; const { queryTransactions } = action.payload;
return { ...state, queryTransactions, showingStartPage: false }; return {
...state,
queryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
}, },
}) })
.addMapper({ .addMapper({
...@@ -319,7 +349,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -319,7 +349,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
// Append new transaction // Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false }; return {
...state,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
}, },
}) })
.addMapper({ .addMapper({
...@@ -333,7 +368,14 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -333,7 +368,14 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
queryIntervals.intervalMs queryIntervals.intervalMs
); );
return { ...state, ...results, history, queryTransactions, showingStartPage: false }; return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
}, },
}) })
.addMapper({ .addMapper({
...@@ -368,12 +410,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -368,12 +410,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
}, },
}) })
.addMapper({ .addMapper({
filter: runQueriesEmptyAction,
mapper: (state): ExploreItemState => {
return { ...state, queryTransactions: [] };
},
})
.addMapper({
filter: scanRangeAction, filter: scanRangeAction,
mapper: (state, action): ExploreItemState => { mapper: (state, action): ExploreItemState => {
return { ...state, scanRange: action.payload.range }; return { ...state, scanRange: action.payload.range };
...@@ -396,6 +432,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -396,6 +432,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
scanning: false, scanning: false,
scanRange: undefined, scanRange: undefined,
scanner: undefined, scanner: undefined,
update: makeInitialUpdateState(),
}; };
}, },
}) })
...@@ -482,6 +519,41 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -482,6 +519,41 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
}) })
.create(); .create();
export const updateChildRefreshState = (
state: Readonly<ExploreItemState>,
payload: LocationUpdate,
exploreId: ExploreId
): ExploreItemState => {
const path = payload.path || '';
const queryState = payload.query[exploreId] as string;
if (!queryState) {
return state;
}
const urlState = parseUrlState(queryState);
if (!state.urlState || path !== '/explore') {
// we only want to refresh when browser back/forward
return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
}
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
return {
...state,
urlState,
update: {
...state.update,
datasource,
queries,
range,
ui,
},
};
};
/** /**
* Global Explore reducer that handles multiple Explore areas (left and right). * Global Explore reducer that handles multiple Explore areas (left and right).
* Actions that have an `exploreId` get routed to the ExploreItemReducer. * Actions that have an `exploreId` get routed to the ExploreItemReducer.
...@@ -493,16 +565,30 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA ...@@ -493,16 +565,30 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
} }
case ActionTypes.SplitOpen: { case ActionTypes.SplitOpen: {
return { ...state, split: true, right: action.payload.itemState }; return { ...state, split: true, right: { ...action.payload.itemState } };
}
case ActionTypes.InitializeExploreSplit: {
return { ...state, split: true };
} }
case ActionTypes.ResetExplore: { case ActionTypes.ResetExplore: {
return initialExploreState; return initialExploreState;
} }
case updateLocation.type: {
const { query } = action.payload;
if (!query || !query[ExploreId.left]) {
return state;
}
const split = query[ExploreId.right] ? true : false;
const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right];
return {
...state,
split,
[ExploreId.left]: updateChildRefreshState(leftState, action.payload, ExploreId.left),
[ExploreId.right]: updateChildRefreshState(rightState, action.payload, ExploreId.right),
};
}
} }
if (action.payload) { if (action.payload) {
......
import _ from 'lodash'; import _ from 'lodash';
import { isVersionGtOrEq } from 'app/core/utils/version'; import { isVersionGtOrEq } from 'app/core/utils/version';
import { InterpolateFunction } from '@grafana/ui';
const index = {}; const index = {};
...@@ -961,24 +962,30 @@ export class FuncInstance { ...@@ -961,24 +962,30 @@ export class FuncInstance {
this.updateText(); this.updateText();
} }
render(metricExp) { render(metricExp: string, replaceVariables: InterpolateFunction): string {
const str = this.def.name + '('; const str = this.def.name + '(';
const parameters = _.map(this.params, (value, index) => { const parameters = _.map(this.params, (value, index) => {
const valueInterpolated = replaceVariables(value);
let paramType; let paramType;
if (index < this.def.params.length) { if (index < this.def.params.length) {
paramType = this.def.params[index].type; paramType = this.def.params[index].type;
} else if (_.get(_.last(this.def.params), 'multiple')) { } else if (_.get(_.last(this.def.params), 'multiple')) {
paramType = _.get(_.last(this.def.params), 'type'); paramType = _.get(_.last(this.def.params), 'type');
} }
// param types that should never be quoted // param types that should never be quoted
if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) { if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) {
return value; return value;
} }
// param types that might be quoted // param types that might be quoted
if (_.includes(['int_or_interval', 'node_or_tag'], paramType) && _.isFinite(+value)) { // To quote variables correctly we need to interpolate it to check if it contains a numeric or string value
return _.toString(+value); if (_.includes(['int_or_interval', 'node_or_tag'], paramType) && _.isFinite(+valueInterpolated)) {
return _.toString(value);
} }
return "'" + value + "'"; return "'" + value + "'";
}); });
......
...@@ -18,6 +18,7 @@ export default class GraphiteQuery { ...@@ -18,6 +18,7 @@ export default class GraphiteQuery {
constructor(datasource, target, templateSrv?, scopedVars?) { constructor(datasource, target, templateSrv?, scopedVars?) {
this.datasource = datasource; this.datasource = datasource;
this.target = target; this.target = target;
this.templateSrv = templateSrv;
this.parseTarget(); this.parseTarget();
this.removeTagValue = '-- remove tag --'; this.removeTagValue = '-- remove tag --';
...@@ -160,7 +161,10 @@ export default class GraphiteQuery { ...@@ -160,7 +161,10 @@ export default class GraphiteQuery {
} }
updateModelTarget(targets) { updateModelTarget(targets) {
// render query const wrapFunction = (target: string, func: any) => {
return func.render(target, this.templateSrv.replace);
};
if (!this.target.textEditor) { if (!this.target.textEditor) {
const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.select metric$/, ''); const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.select metric$/, '');
this.target.target = _.reduce(this.functions, wrapFunction, metricPath); this.target.target = _.reduce(this.functions, wrapFunction, metricPath);
...@@ -302,10 +306,6 @@ export default class GraphiteQuery { ...@@ -302,10 +306,6 @@ export default class GraphiteQuery {
} }
} }
function wrapFunction(target, func) {
return func.render(target);
}
function renderTagString(tag) { function renderTagString(tag) {
return tag.key + tag.operator + tag.value; return tag.key + tag.operator + tag.value;
} }
...@@ -30,66 +30,92 @@ describe('when creating func instance from func names', () => { ...@@ -30,66 +30,92 @@ describe('when creating func instance from func names', () => {
}); });
}); });
function replaceVariablesDummy(str: string) {
return str;
}
describe('when rendering func instance', () => { describe('when rendering func instance', () => {
it('should handle single metric param', () => { it('should handle single metric param', () => {
const func = gfunc.createFuncInstance('sumSeries'); const func = gfunc.createFuncInstance('sumSeries');
expect(func.render('hello.metric')).toEqual('sumSeries(hello.metric)'); expect(func.render('hello.metric', replaceVariablesDummy)).toEqual('sumSeries(hello.metric)');
}); });
it('should include default params if options enable it', () => { it('should include default params if options enable it', () => {
const func = gfunc.createFuncInstance('scaleToSeconds', { const func = gfunc.createFuncInstance('scaleToSeconds', {
withDefaultParams: true, withDefaultParams: true,
}); });
expect(func.render('hello')).toEqual('scaleToSeconds(hello, 1)'); expect(func.render('hello', replaceVariablesDummy)).toEqual('scaleToSeconds(hello, 1)');
}); });
it('should handle int or interval params with number', () => { it('should handle int or interval params with number', () => {
const func = gfunc.createFuncInstance('movingMedian'); const func = gfunc.createFuncInstance('movingMedian');
func.params[0] = '5'; func.params[0] = '5';
expect(func.render('hello')).toEqual('movingMedian(hello, 5)'); expect(func.render('hello', replaceVariablesDummy)).toEqual('movingMedian(hello, 5)');
}); });
it('should handle int or interval params with interval string', () => { it('should handle int or interval params with interval string', () => {
const func = gfunc.createFuncInstance('movingMedian'); const func = gfunc.createFuncInstance('movingMedian');
func.params[0] = '5min'; func.params[0] = '5min';
expect(func.render('hello')).toEqual("movingMedian(hello, '5min')"); expect(func.render('hello', replaceVariablesDummy)).toEqual("movingMedian(hello, '5min')");
}); });
it('should never quote boolean paramater', () => { it('should never quote boolean paramater', () => {
const func = gfunc.createFuncInstance('sortByName'); const func = gfunc.createFuncInstance('sortByName');
func.params[0] = '$natural'; func.params[0] = '$natural';
expect(func.render('hello')).toEqual('sortByName(hello, $natural)'); expect(func.render('hello', replaceVariablesDummy)).toEqual('sortByName(hello, $natural)');
}); });
it('should never quote int paramater', () => { it('should never quote int paramater', () => {
const func = gfunc.createFuncInstance('maximumAbove'); const func = gfunc.createFuncInstance('maximumAbove');
func.params[0] = '$value'; func.params[0] = '$value';
expect(func.render('hello')).toEqual('maximumAbove(hello, $value)'); expect(func.render('hello', replaceVariablesDummy)).toEqual('maximumAbove(hello, $value)');
}); });
it('should never quote node paramater', () => { it('should never quote node paramater', () => {
const func = gfunc.createFuncInstance('aliasByNode'); const func = gfunc.createFuncInstance('aliasByNode');
func.params[0] = '$node'; func.params[0] = '$node';
expect(func.render('hello')).toEqual('aliasByNode(hello, $node)'); expect(func.render('hello', replaceVariablesDummy)).toEqual('aliasByNode(hello, $node)');
}); });
it('should handle metric param and int param and string param', () => { it('should handle metric param and int param and string param', () => {
const func = gfunc.createFuncInstance('groupByNode'); const func = gfunc.createFuncInstance('groupByNode');
func.params[0] = 5; func.params[0] = 5;
func.params[1] = 'avg'; func.params[1] = 'avg';
expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')"); expect(func.render('hello.metric', replaceVariablesDummy)).toEqual("groupByNode(hello.metric, 5, 'avg')");
}); });
it('should handle function with no metric param', () => { it('should handle function with no metric param', () => {
const func = gfunc.createFuncInstance('randomWalk'); const func = gfunc.createFuncInstance('randomWalk');
func.params[0] = 'test'; func.params[0] = 'test';
expect(func.render(undefined)).toEqual("randomWalk('test')"); expect(func.render(undefined, replaceVariablesDummy)).toEqual("randomWalk('test')");
}); });
it('should handle function multiple series params', () => { it('should handle function multiple series params', () => {
const func = gfunc.createFuncInstance('asPercent'); const func = gfunc.createFuncInstance('asPercent');
func.params[0] = '#B'; func.params[0] = '#B';
expect(func.render('#A')).toEqual('asPercent(#A, #B)'); expect(func.render('#A', replaceVariablesDummy)).toEqual('asPercent(#A, #B)');
});
it('should not quote variables that have numeric value', () => {
const func = gfunc.createFuncInstance('movingAverage');
func.params[0] = '$variable';
const replaceVariables = (str: string) => {
return str.replace('$variable', '60');
};
expect(func.render('metric', replaceVariables)).toBe('movingAverage(metric, $variable)');
});
it('should quote variables that have string value', () => {
const func = gfunc.createFuncInstance('movingAverage');
func.params[0] = '$variable';
const replaceVariables = (str: string) => {
return str.replace('$variable', '10min');
};
expect(func.render('metric', replaceVariables)).toBe("movingAverage(metric, '$variable')");
}); });
}); });
......
import gfunc from '../gfunc'; import gfunc from '../gfunc';
import GraphiteQuery from '../graphite_query'; import GraphiteQuery from '../graphite_query';
import { TemplateSrvStub } from 'test/specs/helpers';
describe('Graphite query model', () => { describe('Graphite query model', () => {
const ctx: any = { const ctx: any = {
...@@ -9,7 +10,7 @@ describe('Graphite query model', () => { ...@@ -9,7 +10,7 @@ describe('Graphite query model', () => {
waitForFuncDefsLoaded: jest.fn().mockReturnValue(Promise.resolve(null)), waitForFuncDefsLoaded: jest.fn().mockReturnValue(Promise.resolve(null)),
createFuncInstance: gfunc.createFuncInstance, createFuncInstance: gfunc.createFuncInstance,
}, },
templateSrv: {}, templateSrv: new TemplateSrvStub(),
targets: [], targets: [],
}; };
......
import { uiSegmentSrv } from 'app/core/services/segment_srv'; import { uiSegmentSrv } from 'app/core/services/segment_srv';
import gfunc from '../gfunc'; import gfunc from '../gfunc';
import { GraphiteQueryCtrl } from '../query_ctrl'; import { GraphiteQueryCtrl } from '../query_ctrl';
import { TemplateSrvStub } from 'test/specs/helpers';
describe('GraphiteQueryCtrl', () => { describe('GraphiteQueryCtrl', () => {
const ctx = { const ctx = {
...@@ -30,7 +31,7 @@ describe('GraphiteQueryCtrl', () => { ...@@ -30,7 +31,7 @@ describe('GraphiteQueryCtrl', () => {
{}, {},
{}, {},
new uiSegmentSrv({ trustAsHtml: html => html }, { highlightVariablesAsHtml: () => {} }), new uiSegmentSrv({ trustAsHtml: html => html }, { highlightVariablesAsHtml: () => {} }),
{}, new TemplateSrvStub(),
{} {}
); );
}); });
...@@ -291,7 +292,7 @@ describe('GraphiteQueryCtrl', () => { ...@@ -291,7 +292,7 @@ describe('GraphiteQueryCtrl', () => {
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')"; ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.ctrl.parseTarget(); ctx.ctrl.parseTarget();
ctx.ctrl.removeTag(0); ctx.ctrl.tagChanged({ key: ctx.ctrl.removeTagValue });
}); });
it('should update tags', () => { it('should update tags', () => {
......
...@@ -15,7 +15,7 @@ import { expandRecordingRules } from './language_utils'; ...@@ -15,7 +15,7 @@ import { expandRecordingRules } from './language_utils';
// Types // Types
import { PromQuery } from './types'; import { PromQuery } from './types';
import { DataQueryOptions, DataSourceApi } from '@grafana/ui/src/types'; import { DataQueryOptions, DataSourceApi, AnnotationEvent } from '@grafana/ui/src/types';
import { ExploreUrlState } from 'app/types/explore'; import { ExploreUrlState } from 'app/types/explore';
export class PrometheusDatasource implements DataSourceApi<PromQuery> { export class PrometheusDatasource implements DataSourceApi<PromQuery> {
...@@ -355,10 +355,11 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> { ...@@ -355,10 +355,11 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> {
}) })
.value(); .value();
const dupCheck = {};
for (const value of series.values) { for (const value of series.values) {
const valueIsTrue = value[1] === '1'; // e.g. ALERTS const valueIsTrue = value[1] === '1'; // e.g. ALERTS
if (valueIsTrue || annotation.useValueForTime) { if (valueIsTrue || annotation.useValueForTime) {
const event = { const event: AnnotationEvent = {
annotation: annotation, annotation: annotation,
title: self.resultTransformer.renderTemplate(titleFormat, series.metric), title: self.resultTransformer.renderTemplate(titleFormat, series.metric),
tags: tags, tags: tags,
...@@ -366,9 +367,14 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> { ...@@ -366,9 +367,14 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> {
}; };
if (annotation.useValueForTime) { if (annotation.useValueForTime) {
event['time'] = Math.floor(parseFloat(value[1])); const timestampValue = Math.floor(parseFloat(value[1]));
if (dupCheck[timestampValue]) {
continue;
}
dupCheck[timestampValue] = true;
event.time = timestampValue;
} else { } else {
event['time'] = Math.floor(parseFloat(value[0])) * 1000; event.time = Math.floor(parseFloat(value[0])) * 1000;
} }
eventList.push(event); eventList.push(event);
......
...@@ -3,6 +3,7 @@ import $ from 'jquery'; ...@@ -3,6 +3,7 @@ import $ from 'jquery';
import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.gauge'; import 'vendor/flot/jquery.flot.gauge';
import 'app/features/panel/panellinks/link_srv'; import 'app/features/panel/panellinks/link_srv';
import { getDecimalsForValue } from '@grafana/ui';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import config from 'app/core/config'; import config from 'app/core/config';
...@@ -190,7 +191,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -190,7 +191,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.value = 0; data.value = 0;
data.valueRounded = 0; data.valueRounded = 0;
} else { } else {
const decimalInfo = this.getDecimalsForValue(data.value); const decimalInfo = getDecimalsForValue(data.value);
const formatFunc = getValueFormat(this.panel.format); const formatFunc = getValueFormat(this.panel.format);
data.valueFormatted = formatFunc( data.valueFormatted = formatFunc(
...@@ -243,47 +244,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -243,47 +244,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.render(); this.render();
} }
getDecimalsForValue(value) {
if (_.isNumber(this.panel.decimals)) {
return { decimals: this.panel.decimals, scaledDecimals: null };
}
const delta = value / 2;
let dec = -Math.floor(Math.log(delta) / Math.LN10);
const magn = Math.pow(10, -dec);
const norm = delta / magn; // norm is between 1.0 and 10.0
let size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
// reduce starting decimals if not needed
if (Math.floor(value) === value) {
dec = 0;
}
const result: any = {};
result.decimals = Math.max(0, dec);
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return result;
}
setValues(data) { setValues(data) {
data.flotpairs = []; data.flotpairs = [];
...@@ -319,15 +279,17 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -319,15 +279,17 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.value = this.series[0].stats[this.panel.valueName]; data.value = this.series[0].stats[this.panel.valueName];
data.flotpairs = this.series[0].flotpairs; data.flotpairs = this.series[0].flotpairs;
const decimalInfo = this.getDecimalsForValue(data.value); let decimals = this.panel.decimals;
let scaledDecimals = 0;
if (!this.panel.decimals) {
const decimalInfo = getDecimalsForValue(data.value);
decimals = decimalInfo.decimals;
scaledDecimals = decimalInfo.scaledDecimals;
}
data.valueFormatted = formatFunc( data.valueFormatted = formatFunc(data.value, decimals, scaledDecimals, this.dashboard.isTimezoneUtc());
data.value, data.valueRounded = kbn.roundValue(data.value, decimals);
decimalInfo.decimals,
decimalInfo.scaledDecimals,
this.dashboard.isTimezoneUtc()
);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
} }
// Add $__name variable for using in prefix or postfix // Add $__name variable for using in prefix or postfix
......
...@@ -248,6 +248,17 @@ export interface ExploreItemState { ...@@ -248,6 +248,17 @@ export interface ExploreItemState {
* Currently hidden log series * Currently hidden log series
*/ */
hiddenLogLevels?: LogLevel[]; hiddenLogLevels?: LogLevel[];
urlState: ExploreUrlState;
update: ExploreUpdateState;
}
export interface ExploreUpdateState {
datasource: boolean;
queries: boolean;
range: boolean;
ui: boolean;
} }
export interface ExploreUIState { export interface ExploreUIState {
......
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
const mockStore = configureMockStore([thunk]);
export interface ThunkGiven {
givenThunk: (thunkFunction: any) => ThunkWhen;
}
export interface ThunkWhen {
whenThunkIsDispatched: (...args: any) => ThunkThen;
}
export interface ThunkThen {
thenDispatchedActionsEqual: (actions: Array<ActionOf<any>>) => ThunkWhen;
thenDispatchedActionsAreEqual: (callback: (actions: Array<ActionOf<any>>) => boolean) => ThunkWhen;
thenThereAreNoDispatchedActions: () => ThunkWhen;
}
export const thunkTester = (initialState: any): ThunkGiven => {
const store = mockStore(initialState);
let thunkUnderTest = null;
const givenThunk = (thunkFunction: any): ThunkWhen => {
thunkUnderTest = thunkFunction;
return instance;
};
function whenThunkIsDispatched(...args: any): ThunkThen {
store.dispatch(thunkUnderTest(...arguments));
return instance;
}
const thenDispatchedActionsEqual = (actions: Array<ActionOf<any>>): ThunkWhen => {
const resultingActions = store.getActions();
expect(resultingActions).toEqual(actions);
return instance;
};
const thenDispatchedActionsAreEqual = (callback: (dispathedActions: Array<ActionOf<any>>) => boolean): ThunkWhen => {
const resultingActions = store.getActions();
expect(callback(resultingActions)).toBe(true);
return instance;
};
const thenThereAreNoDispatchedActions = () => {
return thenDispatchedActionsEqual([]);
};
const instance = {
givenThunk,
whenThunkIsDispatched,
thenDispatchedActionsEqual,
thenDispatchedActionsAreEqual,
thenThereAreNoDispatchedActions,
};
return instance;
};
...@@ -172,7 +172,7 @@ export function TemplateSrvStub(this: any) { ...@@ -172,7 +172,7 @@ export function TemplateSrvStub(this: any) {
this.variables = []; this.variables = [];
this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g }; this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g };
this.data = {}; this.data = {};
this.replace = function(text: string) { this.replace = (text: string) => {
return _.template(text, this.templateSettings)(this.data); return _.template(text, this.templateSettings)(this.data);
}; };
this.init = () => {}; this.init = () => {};
......
...@@ -15,7 +15,7 @@ module.exports = function(config) { ...@@ -15,7 +15,7 @@ module.exports = function(config) {
}, },
{ {
expand: true, expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'], src: ['LICENSE', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>-<%= pkg.version %>/', dest: '<%= pkg.name %>-<%= pkg.version %>/',
} }
] ]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment