mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 14812/formgroup-component
This commit is contained in:
@@ -6,6 +6,7 @@ interface Props {
|
||||
autoHide?: boolean;
|
||||
autoHideTimeout?: number;
|
||||
autoHideDuration?: number;
|
||||
autoMaxHeight?: string;
|
||||
hideTracksWhenNotNeeded?: boolean;
|
||||
}
|
||||
|
||||
@@ -18,11 +19,12 @@ export class CustomScrollbar extends PureComponent<Props> {
|
||||
autoHide: true,
|
||||
autoHideTimeout: 200,
|
||||
autoHideDuration: 200,
|
||||
autoMaxHeight: '100%',
|
||||
hideTracksWhenNotNeeded: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { customClassName, children, ...scrollProps } = this.props;
|
||||
const { customClassName, children, autoMaxHeight } = this.props;
|
||||
|
||||
return (
|
||||
<Scrollbars
|
||||
@@ -31,13 +33,12 @@ export class CustomScrollbar extends PureComponent<Props> {
|
||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
autoHeightMin={'0'}
|
||||
autoHeightMax={'100%'}
|
||||
autoHeightMax={autoMaxHeight}
|
||||
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
||||
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||
renderView={props => <div {...props} className="view" />}
|
||||
{...scrollProps}
|
||||
>
|
||||
{children}
|
||||
</Scrollbars>
|
||||
|
||||
@@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
Object {
|
||||
"display": "none",
|
||||
"height": 6,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
"width": 6,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form"
|
||||
className="form-field"
|
||||
>
|
||||
<Component
|
||||
width={11}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { SFC, ReactNode } from 'react';
|
||||
import { Tooltip } from '..';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
interface Props {
|
||||
tooltip?: string;
|
||||
|
||||
@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
|
||||
import IndicatorsContainer from './IndicatorsContainer';
|
||||
import NoOptionsMessage from './NoOptionsMessage';
|
||||
import resetSelectStyles from './resetSelectStyles';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { CustomScrollbar } from '..';
|
||||
|
||||
export interface SelectOptionItem {
|
||||
label?: string;
|
||||
@@ -61,7 +61,7 @@ interface AsyncProps {
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
||||
<CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||
import { BasicGaugeColor } from '../../types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@@ -15,49 +14,160 @@ const setup = (propOverrides?: object) => {
|
||||
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||
};
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should add a base threshold if missing', () => {
|
||||
const instance = setup();
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add threshold', () => {
|
||||
it('should add threshold', () => {
|
||||
it('should not add threshold at index 0', () => {
|
||||
const instance = setup();
|
||||
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
|
||||
});
|
||||
it('should add threshold', () => {
|
||||
const instance = setup();
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
|
||||
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold between first and second index', () => {
|
||||
const instance = setup({
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove threshold', () => {
|
||||
it('should not remove threshold at index 0', () => {
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({ thresholds });
|
||||
|
||||
instance.onRemoveThreshold(thresholds[0]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should remove threshold', () => {
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({
|
||||
thresholds,
|
||||
});
|
||||
|
||||
instance.onRemoveThreshold(thresholds[1]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should update value and resort rows', () => {
|
||||
it('should not change threshold at index 0', () => {
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({ thresholds });
|
||||
|
||||
const mockEvent = { target: { value: 12 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, thresholds[0]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should update value', () => {
|
||||
const instance = setup();
|
||||
const mockThresholds = [
|
||||
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
thresholds: mockThresholds,
|
||||
thresholds,
|
||||
};
|
||||
|
||||
const mockEvent = { target: { value: 78 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
|
||||
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 78, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on blur threshold value', () => {
|
||||
it('should resort rows and update indexes', () => {
|
||||
const instance = setup();
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 78, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
thresholds,
|
||||
};
|
||||
|
||||
instance.onBlur();
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 2, value: 78, color: '#EAB839' },
|
||||
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import tinycolor, { ColorInput } from 'tinycolor2';
|
||||
// import tinycolor, { ColorInput } from 'tinycolor2';
|
||||
|
||||
import { Threshold, BasicGaugeColor } from '../../types';
|
||||
import { Threshold } from '../../types';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
import { colors } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
thresholds: Threshold[];
|
||||
@@ -12,50 +13,43 @@ export interface Props {
|
||||
|
||||
interface State {
|
||||
thresholds: Threshold[];
|
||||
baseColor: string;
|
||||
}
|
||||
|
||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
|
||||
const thresholds: Threshold[] =
|
||||
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
|
||||
this.state = { thresholds };
|
||||
}
|
||||
|
||||
onAddThreshold = (index: number) => {
|
||||
const maxValue = 100; // hardcoded for now before we add the base threshold
|
||||
const minValue = 0; // hardcoded for now before we add the base threshold
|
||||
const { thresholds } = this.state;
|
||||
const maxValue = 100;
|
||||
const minValue = 0;
|
||||
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newThresholds = thresholds.map(threshold => {
|
||||
if (threshold.index >= index) {
|
||||
threshold = {
|
||||
...threshold,
|
||||
index: threshold.index + 1,
|
||||
};
|
||||
const index = threshold.index + 1;
|
||||
threshold = { ...threshold, index };
|
||||
}
|
||||
|
||||
return threshold;
|
||||
});
|
||||
|
||||
// Setting value to a value between the previous thresholds
|
||||
let value;
|
||||
const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
|
||||
const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
|
||||
const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
|
||||
const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
|
||||
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
||||
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
value = maxValue - (maxValue - minValue) / 2;
|
||||
} else if (index === 0 && thresholds.length > 0) {
|
||||
value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
|
||||
} else if (index > newThresholds[newThresholds.length - 1].index) {
|
||||
value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
|
||||
}
|
||||
|
||||
// Set a color that lies between the previous thresholds
|
||||
let color;
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
||||
} else {
|
||||
color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
|
||||
}
|
||||
// Set a color
|
||||
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
|
||||
|
||||
this.setState(
|
||||
{
|
||||
@@ -73,18 +67,40 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
onRemoveThreshold = (threshold: Threshold) => {
|
||||
if (threshold.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
|
||||
prevState => {
|
||||
const newThresholds = prevState.thresholds.map(t => {
|
||||
if (t.index > threshold.index) {
|
||||
const index = t.index - 1;
|
||||
t = { ...t, index };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
|
||||
return {
|
||||
thresholds: newThresholds.filter(t => t !== threshold),
|
||||
};
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||
if (threshold.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { thresholds } = this.state;
|
||||
const parsedValue = parseInt(event.target.value, 10);
|
||||
const value = isNaN(parsedValue) ? null : parsedValue;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, value: event.target.value };
|
||||
t = { ...t, value: value as number };
|
||||
}
|
||||
|
||||
return t;
|
||||
@@ -114,7 +130,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||
onBlur = () => {
|
||||
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
|
||||
this.setState(prevState => {
|
||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||
let index = sortThresholds.length - 1;
|
||||
sortThresholds.forEach(t => {
|
||||
t.index = index--;
|
||||
});
|
||||
return { thresholds: sortThresholds };
|
||||
});
|
||||
|
||||
this.updateGauge();
|
||||
};
|
||||
@@ -129,92 +152,53 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
renderThresholds() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="threshold-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color">
|
||||
{threshold.color && (
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
className="threshold-row-input"
|
||||
type="text"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={threshold.value}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderIndicator() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((t, i) => {
|
||||
return (
|
||||
<div key={`${t.value}-${i}`} className="indicator-section">
|
||||
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderBaseIndicator() {
|
||||
renderInput = (threshold: Threshold) => {
|
||||
const value = threshold.index === 0 ? 'Base' : threshold.value;
|
||||
return (
|
||||
<div className="indicator-section" style={{ height: '100%' }}>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(0)}
|
||||
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
|
||||
/>
|
||||
<div className="thresholds-row-input-inner">
|
||||
<span className="thresholds-row-input-inner-arrow" />
|
||||
<div className="thresholds-row-input-inner-color">
|
||||
{threshold.color && (
|
||||
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="thresholds-row-input-inner-value">
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={value}
|
||||
onBlur={this.onBlur}
|
||||
readOnly={threshold.index === 0}
|
||||
/>
|
||||
</div>
|
||||
{threshold.index > 0 && (
|
||||
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBase() {
|
||||
const baseColor = BasicGaugeColor.Green;
|
||||
|
||||
return (
|
||||
<div className="threshold-row threshold-row-base">
|
||||
<div className="threshold-row-inner threshold-row-inner--base">
|
||||
<div className="threshold-row-color">
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="threshold-row-label">Base</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
<div className="color-indicators">
|
||||
{this.renderIndicator()}
|
||||
{this.renderBaseIndicator()}
|
||||
</div>
|
||||
<div className="threshold-rows">
|
||||
{this.renderThresholds()}
|
||||
{this.renderBase()}
|
||||
</div>
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
|
||||
@@ -1,46 +1,99 @@
|
||||
.thresholds {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.thresholds-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.threshold-rows {
|
||||
margin-left: 5px;
|
||||
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-top-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.threshold-row {
|
||||
.thresholds-row:last-child > .thresholds-row-color-indicator {
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thresholds-row-add-button {
|
||||
align-self: center;
|
||||
margin-right: 5px;
|
||||
color: $green;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: $green;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0d9';
|
||||
color: $input-label-border-color;
|
||||
}
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.threshold-row-inner {
|
||||
border: 1px solid $input-label-border-color;
|
||||
border-radius: $border-radius;
|
||||
.thresholds-row-add-button > i {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.thresholds-row-color-indicator {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.thresholds-row-input {
|
||||
margin-top: 49px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 37px;
|
||||
|
||||
&--base {
|
||||
width: auto;
|
||||
}
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.threshold-row-color {
|
||||
width: 36px;
|
||||
border-right: 1px solid $input-label-border-color;
|
||||
.thresholds-row-input-inner > div {
|
||||
border-left: 1px solid $input-label-border-color;
|
||||
border-top: 1px solid $input-label-border-color;
|
||||
border-bottom: 1px solid $input-label-border-color;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner > *:nth-child(2) {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner > *:last-child {
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner-arrow {
|
||||
align-self: center;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-right: 6px solid $input-label-border-color;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner-value > input {
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner-color {
|
||||
width: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
.threshold-row-color-inner {
|
||||
.thresholds-row-input-inner-color-colorpicker {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -48,56 +101,12 @@
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.threshold-row-input {
|
||||
padding: 8px 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
background-color: $input-label-bg;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.threshold-row-add-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
.thresholds-row-input-inner-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 37px;
|
||||
width: 37px;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
background-color: $input-label-border-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.threshold-row-add {
|
||||
border-right: $border-width solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.indicator-section {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicators {
|
||||
width: 15px;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormField, Label, MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
|
||||
import React, { ChangeEvent, PureComponent } from 'react';
|
||||
|
||||
interface Props {
|
||||
mapping: ValueMap | RangeMap;
|
||||
updateMapping: (mapping) => void;
|
||||
removeMapping: () => void;
|
||||
import { MappingType, ValueMapping } from '../../types';
|
||||
import { FormField, Label, Select } from '..';
|
||||
|
||||
export interface Props {
|
||||
valueMapping: ValueMapping;
|
||||
updateValueMapping: (valueMapping: ValueMapping) => void;
|
||||
removeValueMapping: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
from: string;
|
||||
from?: string;
|
||||
id: number;
|
||||
operator: string;
|
||||
text: string;
|
||||
to: string;
|
||||
to?: string;
|
||||
type: MappingType;
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const mappingOptions = [
|
||||
@@ -23,36 +25,34 @@ const mappingOptions = [
|
||||
];
|
||||
|
||||
export default class MappingRow extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
...props.mapping,
|
||||
};
|
||||
this.state = { ...props.valueMapping };
|
||||
}
|
||||
|
||||
onMappingValueChange = event => {
|
||||
onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ value: event.target.value });
|
||||
};
|
||||
|
||||
onMappingFromChange = event => {
|
||||
onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ from: event.target.value });
|
||||
};
|
||||
|
||||
onMappingToChange = event => {
|
||||
onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ to: event.target.value });
|
||||
};
|
||||
|
||||
onMappingTextChange = event => {
|
||||
onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ text: event.target.value });
|
||||
};
|
||||
|
||||
onMappingTypeChange = mappingType => {
|
||||
onMappingTypeChange = (mappingType: MappingType) => {
|
||||
this.setState({ type: mappingType });
|
||||
};
|
||||
|
||||
updateMapping = () => {
|
||||
this.props.updateMapping({ ...this.state });
|
||||
this.props.updateValueMapping({ ...this.state } as ValueMapping);
|
||||
};
|
||||
|
||||
renderRow() {
|
||||
@@ -65,7 +65,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
label="From"
|
||||
labelWidth={4}
|
||||
inputProps={{
|
||||
onChange: event => this.onMappingFromChange(event),
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingFromChange(event),
|
||||
onBlur: () => this.updateMapping(),
|
||||
value: from,
|
||||
}}
|
||||
@@ -76,7 +76,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
labelWidth={4}
|
||||
inputProps={{
|
||||
onBlur: () => this.updateMapping,
|
||||
onChange: event => this.onMappingToChange(event),
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingToChange(event),
|
||||
value: to,
|
||||
}}
|
||||
inputWidth={8}
|
||||
@@ -101,7 +101,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
labelWidth={4}
|
||||
inputProps={{
|
||||
onBlur: () => this.updateMapping,
|
||||
onChange: event => this.onMappingValueChange(event),
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingValueChange(event),
|
||||
value: value,
|
||||
}}
|
||||
inputWidth={8}
|
||||
@@ -137,7 +137,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
</div>
|
||||
{this.renderRow()}
|
||||
<div className="gf-form">
|
||||
<button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
|
||||
<button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,27 +1,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui';
|
||||
import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
|
||||
|
||||
import ValueMappings from './ValueMappings';
|
||||
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
||||
import { MappingType } from '../../types/panel';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: PanelOptionsProps<GaugeOptions> = {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
mappings: [
|
||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||
],
|
||||
},
|
||||
valueMappings: [
|
||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||
],
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<ValueMappings {...props} />);
|
||||
const wrapper = shallow(<ValueMappingsEditor {...props} />);
|
||||
|
||||
const instance = wrapper.instance() as ValueMappings;
|
||||
const instance = wrapper.instance() as ValueMappingsEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
@@ -40,18 +36,20 @@ describe('Render', () => {
|
||||
describe('On remove mapping', () => {
|
||||
it('Should remove mapping with id 0', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onRemoveMapping(1);
|
||||
|
||||
expect(instance.state.mappings).toEqual([
|
||||
expect(instance.state.valueMappings).toEqual([
|
||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove mapping with id 1', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onRemoveMapping(2);
|
||||
|
||||
expect(instance.state.mappings).toEqual([
|
||||
expect(instance.state.valueMappings).toEqual([
|
||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||
]);
|
||||
});
|
||||
@@ -67,7 +65,7 @@ describe('Next id to add', () => {
|
||||
});
|
||||
|
||||
it('should default to 1', () => {
|
||||
const { instance } = setup({ options: { ...defaultProps.options } });
|
||||
const { instance } = setup({ valueMappings: [] });
|
||||
|
||||
expect(instance.state.nextIdToAdd).toEqual(1);
|
||||
});
|
||||
@@ -1,33 +1,39 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui';
|
||||
|
||||
import MappingRow from './MappingRow';
|
||||
import { MappingType, ValueMapping } from '../../types/panel';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
export interface Props {
|
||||
valueMappings: ValueMapping[];
|
||||
onChange: (valueMappings: ValueMapping[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
mappings: Array<ValueMap | RangeMap>;
|
||||
valueMappings: ValueMapping[];
|
||||
nextIdToAdd: number;
|
||||
}
|
||||
|
||||
export default class ValueMappings extends PureComponent<PanelOptionsProps<GaugeOptions>, State> {
|
||||
constructor(props) {
|
||||
export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const mappings = props.options.mappings;
|
||||
const mappings = props.valueMappings;
|
||||
|
||||
this.state = {
|
||||
mappings: mappings || [],
|
||||
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
|
||||
valueMappings: mappings,
|
||||
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
|
||||
};
|
||||
}
|
||||
|
||||
getMaxIdFromMappings(mappings) {
|
||||
getMaxIdFromValueMappings(mappings: ValueMapping[]) {
|
||||
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
||||
}
|
||||
|
||||
addMapping = () =>
|
||||
this.setState(prevState => ({
|
||||
mappings: [
|
||||
...prevState.mappings,
|
||||
valueMappings: [
|
||||
...prevState.valueMappings,
|
||||
{
|
||||
id: prevState.nextIdToAdd,
|
||||
operator: '',
|
||||
@@ -41,23 +47,23 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
|
||||
nextIdToAdd: prevState.nextIdToAdd + 1,
|
||||
}));
|
||||
|
||||
onRemoveMapping = id => {
|
||||
onRemoveMapping = (id: number) => {
|
||||
this.setState(
|
||||
prevState => ({
|
||||
mappings: prevState.mappings.filter(m => {
|
||||
valueMappings: prevState.valueMappings.filter(m => {
|
||||
return m.id !== id;
|
||||
}),
|
||||
}),
|
||||
() => {
|
||||
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||
this.props.onChange(this.state.valueMappings);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
updateGauge = mapping => {
|
||||
updateGauge = (mapping: ValueMapping) => {
|
||||
this.setState(
|
||||
prevState => ({
|
||||
mappings: prevState.mappings.map(m => {
|
||||
valueMappings: prevState.valueMappings.map(m => {
|
||||
if (m.id === mapping.id) {
|
||||
return { ...mapping };
|
||||
}
|
||||
@@ -66,24 +72,24 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
|
||||
}),
|
||||
}),
|
||||
() => {
|
||||
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||
this.props.onChange(this.state.valueMappings);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { mappings } = this.state;
|
||||
const { valueMappings } = this.state;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Value Mappings">
|
||||
<div>
|
||||
{mappings.length > 0 &&
|
||||
mappings.map((mapping, index) => (
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${mapping.text}-${index}`}
|
||||
mapping={mapping}
|
||||
updateMapping={this.updateGauge}
|
||||
removeMapping={() => this.onRemoveMapping(mapping.id)}
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -7,7 +7,9 @@ exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
mapping={
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
@@ -16,12 +18,12 @@ exports[`Render should render component 1`] = `
|
||||
"value": "20",
|
||||
}
|
||||
}
|
||||
removeMapping={[Function]}
|
||||
updateMapping={[Function]}
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
mapping={
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
@@ -31,8 +33,6 @@ exports[`Render should render component 1`] = `
|
||||
"type": 2,
|
||||
}
|
||||
}
|
||||
removeMapping={[Function]}
|
||||
updateMapping={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -6,4 +6,5 @@
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'ColorPicker/ColorPicker';
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import "FormField/FormField";
|
||||
|
||||
@@ -22,3 +22,4 @@ export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { RangeMap, Threshold, ValueMap } from './panel';
|
||||
|
||||
export interface GaugeOptions {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './series';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './gauge';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { TimeSeries, LoadingState } from './series';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
timeSeries: TimeSeries[];
|
||||
timeRange: TimeRange;
|
||||
@@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
|
||||
renderCounter: number;
|
||||
width: number;
|
||||
height: number;
|
||||
onInterpolate: InterpolateFunction;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
@@ -53,6 +56,8 @@ interface BaseMap {
|
||||
type: MappingType;
|
||||
}
|
||||
|
||||
export type ValueMapping = ValueMap | RangeMap;
|
||||
|
||||
export interface ValueMap extends BaseMap {
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("imguploader")
|
||||
)
|
||||
|
||||
func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
switch setting.ImageUploadProvider {
|
||||
@@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
}
|
||||
|
||||
if setting.ImageUploadProvider != "" {
|
||||
log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||
logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||
}
|
||||
|
||||
return NopImageUploader{}, nil
|
||||
|
||||
@@ -10,13 +10,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/go-stack/stack"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/inconshreveable/log15"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var Root log15.Logger
|
||||
@@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) {
|
||||
Root.Debug(message)
|
||||
}
|
||||
|
||||
func Debug2(message string, v ...interface{}) {
|
||||
Root.Debug(message, v...)
|
||||
}
|
||||
|
||||
func Info(format string, v ...interface{}) {
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
@@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) {
|
||||
Root.Info(message)
|
||||
}
|
||||
|
||||
func Info2(message string, v ...interface{}) {
|
||||
Root.Info(message, v...)
|
||||
}
|
||||
|
||||
func Warn(format string, v ...interface{}) {
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
@@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) {
|
||||
Root.Warn(message)
|
||||
}
|
||||
|
||||
func Warn2(message string, v ...interface{}) {
|
||||
Root.Warn(message, v...)
|
||||
}
|
||||
|
||||
func Error(skip int, format string, v ...interface{}) {
|
||||
Root.Error(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func Error2(message string, v ...interface{}) {
|
||||
Root.Error(message, v...)
|
||||
}
|
||||
|
||||
func Critical(skip int, format string, v ...interface{}) {
|
||||
Root.Crit(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ func init() {
|
||||
bus.AddHandler("auth", UpsertUser)
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("login.ext_user")
|
||||
)
|
||||
|
||||
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
extUser := cmd.ExternalUser
|
||||
|
||||
@@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
return bus.Dispatch(updateCmd)
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
|
||||
defer func() {
|
||||
err := imageFile.Close()
|
||||
if err != nil {
|
||||
log.Error2("Could not close Telegram inline image.", "err", err)
|
||||
this.log.Error("Could not close Telegram inline image.", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -18,9 +18,12 @@ type NotificationTestCommand struct {
|
||||
Settings *simplejson.Json
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("alerting.testnotification")
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("alerting", handleNotificationTestCommand)
|
||||
|
||||
}
|
||||
|
||||
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
@@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
notifiers, err := InitNotifier(model)
|
||||
|
||||
if err != nil {
|
||||
log.Error2("Failed to create notifier", "error", err.Error())
|
||||
logger.Error("Failed to create notifier", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,9 @@ export function dropdownTypeahead2($compile) {
|
||||
link: ($scope, elem, attrs) => {
|
||||
const $input = $(inputTemplate);
|
||||
const $button = $(buttonTemplate);
|
||||
const timeoutId = {
|
||||
blur: null
|
||||
};
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
@@ -177,6 +180,14 @@ export function dropdownTypeahead2($compile) {
|
||||
[]
|
||||
);
|
||||
|
||||
const closeDropdownMenu = () => {
|
||||
$input.hide();
|
||||
$input.val('');
|
||||
$button.show();
|
||||
$button.focus();
|
||||
elem.removeClass('open');
|
||||
};
|
||||
|
||||
$scope.menuItemSelected = (index, subIndex) => {
|
||||
const menuItem = $scope.menuItems[index];
|
||||
const payload: any = { $item: menuItem };
|
||||
@@ -184,6 +195,7 @@ export function dropdownTypeahead2($compile) {
|
||||
payload.$subItem = menuItem.submenu[subIndex];
|
||||
}
|
||||
$scope.dropdownTypeaheadOnSelect(payload);
|
||||
closeDropdownMenu();
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
@@ -223,16 +235,15 @@ export function dropdownTypeahead2($compile) {
|
||||
elem.toggleClass('open', $input.val() === '');
|
||||
});
|
||||
|
||||
elem.mousedown((evt: Event) => {
|
||||
evt.preventDefault();
|
||||
timeoutId.blur = null;
|
||||
});
|
||||
|
||||
$input.blur(() => {
|
||||
$input.hide();
|
||||
$input.val('');
|
||||
$button.show();
|
||||
$button.focus();
|
||||
// clicking the function dropdown menu won't
|
||||
// work if you remove class at once
|
||||
setTimeout(() => {
|
||||
elem.removeClass('open');
|
||||
}, 200);
|
||||
timeoutId.blur = setTimeout(() => {
|
||||
closeDropdownMenu();
|
||||
}, 1);
|
||||
});
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
|
||||
@@ -6,26 +6,13 @@ import {
|
||||
clearHistory,
|
||||
hasNonEmptyQuery,
|
||||
} from './explore';
|
||||
import { ExploreState } from 'app/types/explore';
|
||||
import { ExploreUrlState } from 'app/types/explore';
|
||||
import store from 'app/core/store';
|
||||
|
||||
const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
exploreDatasources: [],
|
||||
graphInterval: 1000,
|
||||
history: [],
|
||||
initialQueries: [],
|
||||
queryTransactions: [],
|
||||
queries: [],
|
||||
range: DEFAULT_RANGE,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
};
|
||||
|
||||
describe('state functions', () => {
|
||||
@@ -68,21 +55,19 @@ describe('state functions', () => {
|
||||
it('returns url parameter value for a state object', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
initialDatasource: 'foo',
|
||||
datasource: 'foo',
|
||||
queries: [
|
||||
{
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
range: {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
},
|
||||
initialQueries: [
|
||||
{
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(serializeStateToUrlParam(state)).toBe(
|
||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||
@@ -93,21 +78,19 @@ describe('state functions', () => {
|
||||
it('returns url parameter value for a state object', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
initialDatasource: 'foo',
|
||||
datasource: 'foo',
|
||||
queries: [
|
||||
{
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
range: {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
},
|
||||
initialQueries: [
|
||||
{
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
||||
@@ -119,35 +102,24 @@ describe('state functions', () => {
|
||||
it('can parse the serialized state into the original state', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
initialDatasource: 'foo',
|
||||
datasource: 'foo',
|
||||
queries: [
|
||||
{
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
range: {
|
||||
from: 'now - 5h',
|
||||
to: 'now',
|
||||
},
|
||||
initialQueries: [
|
||||
{
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
const serialized = serializeStateToUrlParam(state);
|
||||
const parsed = parseUrlState(serialized);
|
||||
|
||||
// Account for datasource vs datasourceName
|
||||
const { datasource, queries, ...rest } = parsed;
|
||||
const resultState = {
|
||||
...rest,
|
||||
datasource: DEFAULT_EXPLORE_STATE.datasource,
|
||||
initialDatasource: datasource,
|
||||
initialQueries: queries,
|
||||
};
|
||||
|
||||
expect(state).toMatchObject(resultState);
|
||||
expect(state).toMatchObject(parsed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { colors } from '@grafana/ui';
|
||||
import { colors, RawTimeRange, IntervalValues } from '@grafana/ui';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import store from 'app/core/store';
|
||||
@@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
|
||||
import { DataQuery, DataSourceApi } from 'app/types/series';
|
||||
import { RawTimeRange, IntervalValues } from '@grafana/ui';
|
||||
import {
|
||||
ExploreUrlState,
|
||||
HistoryItem,
|
||||
QueryTransaction,
|
||||
ResultType,
|
||||
QueryIntervals,
|
||||
QueryOptions,
|
||||
} from 'app/types/explore';
|
||||
import { DataQuery } from 'app/types/series';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
@@ -19,6 +26,8 @@ export const DEFAULT_RANGE = {
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
||||
|
||||
/**
|
||||
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
||||
*
|
||||
@@ -77,7 +86,63 @@ export async function getExploreUrl(
|
||||
return url;
|
||||
}
|
||||
|
||||
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||
export function buildQueryTransaction(
|
||||
query: DataQuery,
|
||||
rowIndex: number,
|
||||
resultType: ResultType,
|
||||
queryOptions: QueryOptions,
|
||||
range: RawTimeRange,
|
||||
queryIntervals: QueryIntervals,
|
||||
scanning: boolean
|
||||
): QueryTransaction {
|
||||
const { interval, intervalMs } = queryIntervals;
|
||||
|
||||
const configuredQueries = [
|
||||
{
|
||||
...query,
|
||||
...queryOptions,
|
||||
},
|
||||
];
|
||||
|
||||
// Clone range for query request
|
||||
// const queryRange: RawTimeRange = { ...range };
|
||||
// const { from, to, raw } = this.timeSrv.timeRange();
|
||||
// Most datasource is using `panelId + query.refId` for cancellation logic.
|
||||
// Using `format` here because it relates to the view panel that the request is for.
|
||||
// However, some datasources don't use `panelId + query.refId`, but only `panelId`.
|
||||
// Therefore panel id has to be unique.
|
||||
const panelId = `${queryOptions.format}-${query.key}`;
|
||||
|
||||
const options = {
|
||||
interval,
|
||||
intervalMs,
|
||||
panelId,
|
||||
targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
|
||||
range: {
|
||||
from: dateMath.parse(range.from, false),
|
||||
to: dateMath.parse(range.to, true),
|
||||
raw: range,
|
||||
},
|
||||
rangeRaw: range,
|
||||
scopedVars: {
|
||||
__interval: { text: interval, value: interval },
|
||||
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
options,
|
||||
query,
|
||||
resultType,
|
||||
rowIndex,
|
||||
scanning,
|
||||
id: generateKey(), // reusing for unique ID
|
||||
done: false,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||
|
||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
if (initial) {
|
||||
@@ -103,12 +168,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
||||
}
|
||||
|
||||
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
|
||||
const urlState: ExploreUrlState = {
|
||||
datasource: state.initialDatasource,
|
||||
queries: state.initialQueries.map(clearQueryKeys),
|
||||
range: state.range,
|
||||
};
|
||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||
if (compact) {
|
||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
||||
}
|
||||
@@ -123,7 +183,7 @@ export function generateRefId(index = 0): string {
|
||||
return `${index + 1}`;
|
||||
}
|
||||
|
||||
export function generateQueryKeys(index = 0): { refId: string; key: string } {
|
||||
export function generateEmptyQuery(index = 0): { refId: string; key: string } {
|
||||
return { refId: generateRefId(index), key: generateKey(index) };
|
||||
}
|
||||
|
||||
@@ -132,20 +192,23 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } {
|
||||
*/
|
||||
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
||||
if (queries && typeof queries === 'object' && queries.length > 0) {
|
||||
return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
|
||||
return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
|
||||
}
|
||||
return [{ ...generateQueryKeys() }];
|
||||
return [{ ...generateEmptyQuery() }];
|
||||
}
|
||||
|
||||
/**
|
||||
* A target is non-empty when it has keys (with non-empty values) other than refId and key.
|
||||
*/
|
||||
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
|
||||
return queries.some(
|
||||
query =>
|
||||
Object.keys(query)
|
||||
.map(k => query[k])
|
||||
.filter(v => v).length > 2
|
||||
return (
|
||||
queries &&
|
||||
queries.some(
|
||||
query =>
|
||||
Object.keys(query)
|
||||
.map(k => query[k])
|
||||
.filter(v => v).length > 2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,8 +243,8 @@ export function calculateResultsFromQueryTransactions(
|
||||
};
|
||||
}
|
||||
|
||||
export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
|
||||
if (!datasource || !resolution) {
|
||||
export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
|
||||
if (!resolution) {
|
||||
return { interval: '1s', intervalMs: 1000 };
|
||||
}
|
||||
|
||||
@@ -190,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res
|
||||
to: parseDate(range.to, true),
|
||||
};
|
||||
|
||||
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||
return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
|
||||
}
|
||||
|
||||
export function makeTimeSeriesList(dataList) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
import variables from 'sass/_variables.scss';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -78,6 +79,10 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
onInterpolate = (value: string, format?: string) => {
|
||||
return templateSrv.replace(value, this.props.panel.scopedVars, format);
|
||||
};
|
||||
|
||||
get isVisible() {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
@@ -124,9 +129,10 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeSeries={timeSeries}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||
width={width - 2 * variables.panelHorizontalPadding }
|
||||
width={width - 2 * variables.panelHorizontalPadding}
|
||||
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
||||
renderCounter={renderCounter}
|
||||
onInterpolate={this.onInterpolate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import classNames from 'classnames';
|
||||
|
||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
@@ -45,7 +46,9 @@ export class PanelHeader extends Component<Props, State> {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
const { panel, dashboard, timeInfo } = this.props;
|
||||
const { panel, dashboard, timeInfo, scopedVars } = this.props;
|
||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelHeaderCorner
|
||||
@@ -65,7 +68,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">
|
||||
{panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||
</span>
|
||||
|
||||
{this.state.panelMenuOpen && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
61
public/app/features/explore/GraphContainer.tsx
Normal file
61
public/app/features/explore/GraphContainer.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleGraph } from './state/actions';
|
||||
import Graph from './Graph';
|
||||
import Panel from './Panel';
|
||||
|
||||
interface GraphContainerProps {
|
||||
onChangeTime: (range: TimeRange) => void;
|
||||
exploreId: ExploreId;
|
||||
graphResult?: any[];
|
||||
loading: boolean;
|
||||
range: RawTimeRange;
|
||||
showingGraph: boolean;
|
||||
showingTable: boolean;
|
||||
split: boolean;
|
||||
toggleGraph: typeof toggleGraph;
|
||||
}
|
||||
|
||||
export class GraphContainer extends PureComponent<GraphContainerProps> {
|
||||
onClickGraphButton = () => {
|
||||
this.props.toggleGraph(this.props.exploreId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
|
||||
const graphHeight = showingGraph && showingTable ? '200px' : '400px';
|
||||
return (
|
||||
<Panel label="Graph" isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
|
||||
<Graph
|
||||
data={graphResult}
|
||||
height={graphHeight}
|
||||
id={`explore-graph-${exploreId}`}
|
||||
onChangeTime={onChangeTime}
|
||||
range={range}
|
||||
split={split}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
const explore = state.explore;
|
||||
const { split } = explore;
|
||||
const item: ExploreItemState = explore[exploreId];
|
||||
const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
|
||||
const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
|
||||
return { graphResult, loading, range, showingGraph, showingTable, split };
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleGraph,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));
|
||||
@@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
|
||||
|
||||
interface LogsProps {
|
||||
data: LogsModel;
|
||||
exploreId: string;
|
||||
highlighterExpressions: string[];
|
||||
loading: boolean;
|
||||
position: string;
|
||||
range?: RawTimeRange;
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
@@ -348,10 +348,10 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
exploreId,
|
||||
highlighterExpressions,
|
||||
loading = false,
|
||||
onClickLabel,
|
||||
position,
|
||||
range,
|
||||
scanning,
|
||||
scanRange,
|
||||
@@ -400,7 +400,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
data={data.series}
|
||||
height="100px"
|
||||
range={range}
|
||||
id={`explore-logs-graph-${position}`}
|
||||
id={`explore-logs-graph-${exploreId}`}
|
||||
onChangeTime={this.props.onChangeTime}
|
||||
onToggleSeries={this.onToggleLogLevel}
|
||||
userOptions={graphOptions}
|
||||
|
||||
91
public/app/features/explore/LogsContainer.tsx
Normal file
91
public/app/features/explore/LogsContainer.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { LogsModel } from 'app/core/logs_model';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleLogs } from './state/actions';
|
||||
import Logs from './Logs';
|
||||
import Panel from './Panel';
|
||||
|
||||
interface LogsContainerProps {
|
||||
exploreId: ExploreId;
|
||||
loading: boolean;
|
||||
logsHighlighterExpressions?: string[];
|
||||
logsResult?: LogsModel;
|
||||
onChangeTime: (range: TimeRange) => void;
|
||||
onClickLabel: (key: string, value: string) => void;
|
||||
onStartScanning: () => void;
|
||||
onStopScanning: () => void;
|
||||
range: RawTimeRange;
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
showingLogs: boolean;
|
||||
toggleLogs: typeof toggleLogs;
|
||||
}
|
||||
|
||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
onClickLogsButton = () => {
|
||||
this.props.toggleLogs(this.props.exploreId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
exploreId,
|
||||
loading,
|
||||
logsHighlighterExpressions,
|
||||
logsResult,
|
||||
onChangeTime,
|
||||
onClickLabel,
|
||||
onStartScanning,
|
||||
onStopScanning,
|
||||
range,
|
||||
showingLogs,
|
||||
scanning,
|
||||
scanRange,
|
||||
} = this.props;
|
||||
return (
|
||||
<Panel label="Logs" loading={loading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
|
||||
<Logs
|
||||
data={logsResult}
|
||||
exploreId={exploreId}
|
||||
key={logsResult.id}
|
||||
highlighterExpressions={logsHighlighterExpressions}
|
||||
loading={loading}
|
||||
onChangeTime={onChangeTime}
|
||||
onClickLabel={onClickLabel}
|
||||
onStartScanning={onStartScanning}
|
||||
onStopScanning={onStopScanning}
|
||||
range={range}
|
||||
scanning={scanning}
|
||||
scanRange={scanRange}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
const explore = state.explore;
|
||||
const item: ExploreItemState = explore[exploreId];
|
||||
const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item;
|
||||
const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
|
||||
return {
|
||||
loading,
|
||||
logsHighlighterExpressions,
|
||||
logsResult,
|
||||
scanning,
|
||||
scanRange,
|
||||
showingLogs,
|
||||
range,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleLogs,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
|
||||
@@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||
getNextQueryLetter: x => '',
|
||||
},
|
||||
hideEditorRowActions: true,
|
||||
...getIntervals(range, datasource, null), // Possible to get resolution?
|
||||
...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
163
public/app/features/explore/QueryRow.tsx
Normal file
163
public/app/features/explore/QueryRow.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { DataQuery, StoreState } from 'app/types';
|
||||
|
||||
// import DefaultQueryField from './QueryField';
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||
import {
|
||||
addQueryRow,
|
||||
changeQuery,
|
||||
highlightLogsExpression,
|
||||
modifyQueries,
|
||||
removeQueryRow,
|
||||
runQueries,
|
||||
} from './state/actions';
|
||||
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||
if (transaction) {
|
||||
return transaction.hints[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface QueryRowProps {
|
||||
addQueryRow: typeof addQueryRow;
|
||||
changeQuery: typeof changeQuery;
|
||||
className?: string;
|
||||
exploreId: ExploreId;
|
||||
datasourceInstance: any;
|
||||
highlightLogsExpression: typeof highlightLogsExpression;
|
||||
history: HistoryItem[];
|
||||
index: number;
|
||||
initialQuery: DataQuery;
|
||||
modifyQueries: typeof modifyQueries;
|
||||
queryTransactions: QueryTransaction[];
|
||||
exploreEvents: Emitter;
|
||||
range: RawTimeRange;
|
||||
removeQueryRow: typeof removeQueryRow;
|
||||
runQueries: typeof runQueries;
|
||||
}
|
||||
|
||||
export class QueryRow extends PureComponent<QueryRowProps> {
|
||||
onExecuteQuery = () => {
|
||||
const { exploreId } = this.props;
|
||||
this.props.runQueries(exploreId);
|
||||
};
|
||||
|
||||
onChangeQuery = (query: DataQuery, override?: boolean) => {
|
||||
const { datasourceInstance, exploreId, index } = this.props;
|
||||
this.props.changeQuery(exploreId, query, index, override);
|
||||
if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
|
||||
// Live preview of log search matches. Only use on first row for now
|
||||
this.updateLogsHighlights(query);
|
||||
}
|
||||
};
|
||||
|
||||
onClickAddButton = () => {
|
||||
const { exploreId, index } = this.props;
|
||||
this.props.addQueryRow(exploreId, index);
|
||||
};
|
||||
|
||||
onClickClearButton = () => {
|
||||
this.onChangeQuery(null, true);
|
||||
};
|
||||
|
||||
onClickHintFix = action => {
|
||||
const { datasourceInstance, exploreId, index } = this.props;
|
||||
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
||||
const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
|
||||
this.props.modifyQueries(exploreId, action, index, modifier);
|
||||
}
|
||||
};
|
||||
|
||||
onClickRemoveButton = () => {
|
||||
const { exploreId, index } = this.props;
|
||||
this.props.removeQueryRow(exploreId, index);
|
||||
};
|
||||
|
||||
updateLogsHighlights = _.debounce((value: DataQuery) => {
|
||||
const { datasourceInstance } = this.props;
|
||||
if (datasourceInstance.getHighlighterExpression) {
|
||||
const expressions = [datasourceInstance.getHighlighterExpression(value)];
|
||||
this.props.highlightLogsExpression(this.props.exploreId, expressions);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
render() {
|
||||
const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
|
||||
const transactions = queryTransactions.filter(t => t.rowIndex === index);
|
||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||
const hint = getFirstHintFromTransactions(transactions);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
|
||||
return (
|
||||
<div className="query-row">
|
||||
<div className="query-row-status">
|
||||
<QueryTransactionStatus transactions={transactions} />
|
||||
</div>
|
||||
<div className="query-row-field">
|
||||
{QueryField ? (
|
||||
<QueryField
|
||||
datasource={datasourceInstance}
|
||||
error={queryError}
|
||||
hint={hint}
|
||||
initialQuery={initialQuery}
|
||||
history={history}
|
||||
onClickHintFix={this.onClickHintFix}
|
||||
onPressEnter={this.onExecuteQuery}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
/>
|
||||
) : (
|
||||
<QueryEditor
|
||||
datasource={datasourceInstance}
|
||||
error={queryError}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
onExecuteQuery={this.onExecuteQuery}
|
||||
initialQuery={initialQuery}
|
||||
exploreEvents={exploreEvents}
|
||||
range={range}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="query-row-tools">
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId, index }) {
|
||||
const explore = state.explore;
|
||||
const item: ExploreItemState = explore[exploreId];
|
||||
const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
|
||||
const initialQuery = initialQueries[index];
|
||||
return { datasourceInstance, history, initialQuery, queryTransactions, range };
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addQueryRow,
|
||||
changeQuery,
|
||||
highlightLogsExpression,
|
||||
modifyQueries,
|
||||
removeQueryRow,
|
||||
runQueries,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));
|
||||
@@ -1,159 +1,25 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { DataQuery } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
// import DefaultQueryField from './QueryField';
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||
import { DataSource, DataQuery } from 'app/types';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import QueryRow from './QueryRow';
|
||||
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||
if (transaction) {
|
||||
return transaction.hints[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface QueryRowEventHandlers {
|
||||
onAddQueryRow: (index: number) => void;
|
||||
onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
|
||||
onClickHintFix: (action: object, index?: number) => void;
|
||||
onExecuteQuery: () => void;
|
||||
onRemoveQueryRow: (index: number) => void;
|
||||
}
|
||||
|
||||
interface QueryRowCommonProps {
|
||||
interface QueryRowsProps {
|
||||
className?: string;
|
||||
datasource: DataSource;
|
||||
history: HistoryItem[];
|
||||
transactions: QueryTransaction[];
|
||||
exploreEvents: Emitter;
|
||||
range: RawTimeRange;
|
||||
exploreId: ExploreId;
|
||||
initialQueries: DataQuery[];
|
||||
}
|
||||
|
||||
type QueryRowProps = QueryRowCommonProps &
|
||||
QueryRowEventHandlers & {
|
||||
index: number;
|
||||
initialQuery: DataQuery;
|
||||
};
|
||||
|
||||
class QueryRow extends PureComponent<QueryRowProps> {
|
||||
onExecuteQuery = () => {
|
||||
const { onExecuteQuery } = this.props;
|
||||
onExecuteQuery();
|
||||
};
|
||||
|
||||
onChangeQuery = (value: DataQuery, override?: boolean) => {
|
||||
const { index, onChangeQuery } = this.props;
|
||||
if (onChangeQuery) {
|
||||
onChangeQuery(value, index, override);
|
||||
}
|
||||
};
|
||||
|
||||
onClickAddButton = () => {
|
||||
const { index, onAddQueryRow } = this.props;
|
||||
if (onAddQueryRow) {
|
||||
onAddQueryRow(index);
|
||||
}
|
||||
};
|
||||
|
||||
onClickClearButton = () => {
|
||||
this.onChangeQuery(null, true);
|
||||
};
|
||||
|
||||
onClickHintFix = action => {
|
||||
const { index, onClickHintFix } = this.props;
|
||||
if (onClickHintFix) {
|
||||
onClickHintFix(action, index);
|
||||
}
|
||||
};
|
||||
|
||||
onClickRemoveButton = () => {
|
||||
const { index, onRemoveQueryRow } = this.props;
|
||||
if (onRemoveQueryRow) {
|
||||
onRemoveQueryRow(index);
|
||||
}
|
||||
};
|
||||
|
||||
onPressEnter = () => {
|
||||
const { onExecuteQuery } = this.props;
|
||||
if (onExecuteQuery) {
|
||||
onExecuteQuery();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props;
|
||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||
const hint = getFirstHintFromTransactions(transactions);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
const QueryField = datasource.pluginExports.ExploreQueryField;
|
||||
return (
|
||||
<div className="query-row">
|
||||
<div className="query-row-status">
|
||||
<QueryTransactionStatus transactions={transactions} />
|
||||
</div>
|
||||
<div className="query-row-field">
|
||||
{QueryField ? (
|
||||
<QueryField
|
||||
datasource={datasource}
|
||||
error={queryError}
|
||||
hint={hint}
|
||||
initialQuery={initialQuery}
|
||||
history={history}
|
||||
onClickHintFix={this.onClickHintFix}
|
||||
onPressEnter={this.onPressEnter}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
/>
|
||||
) : (
|
||||
<QueryEditor
|
||||
datasource={datasource}
|
||||
error={queryError}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
onExecuteQuery={this.onExecuteQuery}
|
||||
initialQuery={initialQuery}
|
||||
exploreEvents={exploreEvents}
|
||||
range={range}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="query-row-tools">
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type QueryRowsProps = QueryRowCommonProps &
|
||||
QueryRowEventHandlers & {
|
||||
initialQueries: DataQuery[];
|
||||
};
|
||||
|
||||
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
||||
render() {
|
||||
const { className = '', initialQueries, transactions, ...handlers } = this.props;
|
||||
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
|
||||
return (
|
||||
<div className={className}>
|
||||
{initialQueries.map((query, index) => (
|
||||
<QueryRow
|
||||
key={query.key}
|
||||
index={index}
|
||||
initialQuery={query}
|
||||
transactions={transactions.filter(t => t.rowIndex === index)}
|
||||
{...handlers}
|
||||
/>
|
||||
// TODO instead of relying on initialQueries, move to react key list in redux
|
||||
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
49
public/app/features/explore/TableContainer.tsx
Normal file
49
public/app/features/explore/TableContainer.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleGraph } from './state/actions';
|
||||
import Table from './Table';
|
||||
import Panel from './Panel';
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
interface TableContainerProps {
|
||||
exploreId: ExploreId;
|
||||
loading: boolean;
|
||||
onClickCell: (key: string, value: string) => void;
|
||||
showingTable: boolean;
|
||||
tableResult?: TableModel;
|
||||
toggleGraph: typeof toggleGraph;
|
||||
}
|
||||
|
||||
export class TableContainer extends PureComponent<TableContainerProps> {
|
||||
onClickTableButton = () => {
|
||||
this.props.toggleGraph(this.props.exploreId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, onClickCell, showingTable, tableResult } = this.props;
|
||||
return (
|
||||
<Panel label="Table" loading={loading} isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||
<Table data={tableResult} loading={loading} onClickCell={onClickCell} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
const explore = state.explore;
|
||||
const item: ExploreItemState = explore[exploreId];
|
||||
const { queryTransactions, showingTable, tableResult } = item;
|
||||
const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
|
||||
return { loading, showingTable, tableResult };
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleGraph,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
||||
@@ -3,91 +3,56 @@ import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreState } from 'app/types/explore';
|
||||
import { ExploreId, ExploreUrlState } from 'app/types/explore';
|
||||
import { parseUrlState } from 'app/core/utils/explore';
|
||||
|
||||
import { initializeExploreSplit } from './state/actions';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import Explore from './Explore';
|
||||
|
||||
interface WrapperProps {
|
||||
backendSrv?: any;
|
||||
datasourceSrv?: any;
|
||||
initializeExploreSplit: typeof initializeExploreSplit;
|
||||
split: boolean;
|
||||
updateLocation: typeof updateLocation;
|
||||
urlStates: { [key: string]: string };
|
||||
}
|
||||
|
||||
interface WrapperState {
|
||||
split: boolean;
|
||||
splitState: ExploreState;
|
||||
}
|
||||
|
||||
const STATE_KEY_LEFT = 'state';
|
||||
const STATE_KEY_RIGHT = 'stateRight';
|
||||
|
||||
export class Wrapper extends Component<WrapperProps, WrapperState> {
|
||||
urlStates: { [key: string]: string };
|
||||
export class Wrapper extends Component<WrapperProps> {
|
||||
initialSplit: boolean;
|
||||
urlStates: { [key: string]: ExploreUrlState };
|
||||
|
||||
constructor(props: WrapperProps) {
|
||||
super(props);
|
||||
this.urlStates = props.urlStates;
|
||||
this.state = {
|
||||
split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
|
||||
splitState: undefined,
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
onChangeSplit = (split: boolean, splitState: ExploreState) => {
|
||||
this.setState({ split, splitState });
|
||||
// When closing split, remove URL state for split part
|
||||
if (!split) {
|
||||
delete this.urlStates[STATE_KEY_RIGHT];
|
||||
this.props.updateLocation({
|
||||
query: this.urlStates,
|
||||
});
|
||||
componentDidMount() {
|
||||
if (this.initialSplit) {
|
||||
this.props.initializeExploreSplit();
|
||||
}
|
||||
};
|
||||
|
||||
onSaveState = (key: string, state: ExploreState) => {
|
||||
const urlState = serializeStateToUrlParam(state, true);
|
||||
this.urlStates[key] = urlState;
|
||||
this.props.updateLocation({
|
||||
query: this.urlStates,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasourceSrv } = this.props;
|
||||
// State overrides for props from first Explore
|
||||
const { split, splitState } = this.state;
|
||||
const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
|
||||
const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
|
||||
const { split } = this.props;
|
||||
const { leftState, rightState } = this.urlStates;
|
||||
|
||||
return (
|
||||
<div className="explore-wrapper">
|
||||
<ErrorBoundary>
|
||||
<Explore
|
||||
datasourceSrv={datasourceSrv}
|
||||
onChangeSplit={this.onChangeSplit}
|
||||
onSaveState={this.onSaveState}
|
||||
position="left"
|
||||
split={split}
|
||||
stateKey={STATE_KEY_LEFT}
|
||||
urlState={urlStateLeft}
|
||||
/>
|
||||
<Explore exploreId={ExploreId.left} urlState={leftState} />
|
||||
</ErrorBoundary>
|
||||
{split && (
|
||||
<ErrorBoundary>
|
||||
<Explore
|
||||
datasourceSrv={datasourceSrv}
|
||||
onChangeSplit={this.onChangeSplit}
|
||||
onSaveState={this.onSaveState}
|
||||
position="right"
|
||||
split={split}
|
||||
splitState={splitState}
|
||||
stateKey={STATE_KEY_RIGHT}
|
||||
urlState={urlStateRight}
|
||||
/>
|
||||
<Explore exploreId={ExploreId.right} urlState={rightState} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</div>
|
||||
@@ -95,11 +60,14 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
urlStates: state.location.query,
|
||||
});
|
||||
const mapStateToProps = (state: StoreState) => {
|
||||
const urlStates = state.location.query;
|
||||
const { split } = state.explore;
|
||||
return { split, urlStates };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initializeExploreSplit,
|
||||
updateLocation,
|
||||
};
|
||||
|
||||
|
||||
302
public/app/features/explore/state/actionTypes.ts
Normal file
302
public/app/features/explore/state/actionTypes.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
import {
|
||||
ExploreId,
|
||||
ExploreItemState,
|
||||
HistoryItem,
|
||||
RangeScanner,
|
||||
ResultType,
|
||||
QueryTransaction,
|
||||
} from 'app/types/explore';
|
||||
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||
import { DataQuery } from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
AddQueryRow = 'explore/ADD_QUERY_ROW',
|
||||
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
|
||||
ChangeQuery = 'explore/CHANGE_QUERY',
|
||||
ChangeSize = 'explore/CHANGE_SIZE',
|
||||
ChangeTime = 'explore/CHANGE_TIME',
|
||||
ClearQueries = 'explore/CLEAR_QUERIES',
|
||||
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
|
||||
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
|
||||
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
|
||||
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
|
||||
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
|
||||
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
|
||||
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
|
||||
ModifyQueries = 'explore/MODIFY_QUERIES',
|
||||
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
|
||||
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
|
||||
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
|
||||
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
|
||||
RunQueries = 'explore/RUN_QUERIES',
|
||||
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
|
||||
ScanRange = 'explore/SCAN_RANGE',
|
||||
ScanStart = 'explore/SCAN_START',
|
||||
ScanStop = 'explore/SCAN_STOP',
|
||||
SetQueries = 'explore/SET_QUERIES',
|
||||
SplitClose = 'explore/SPLIT_CLOSE',
|
||||
SplitOpen = 'explore/SPLIT_OPEN',
|
||||
StateSave = 'explore/STATE_SAVE',
|
||||
ToggleGraph = 'explore/TOGGLE_GRAPH',
|
||||
ToggleLogs = 'explore/TOGGLE_LOGS',
|
||||
ToggleTable = 'explore/TOGGLE_TABLE',
|
||||
}
|
||||
|
||||
export interface AddQueryRowAction {
|
||||
type: ActionTypes.AddQueryRow;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
index: number;
|
||||
query: DataQuery;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChangeQueryAction {
|
||||
type: ActionTypes.ChangeQuery;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
query: DataQuery;
|
||||
index: number;
|
||||
override: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChangeSizeAction {
|
||||
type: ActionTypes.ChangeSize;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChangeTimeAction {
|
||||
type: ActionTypes.ChangeTime;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
range: TimeRange;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClearQueriesAction {
|
||||
type: ActionTypes.ClearQueries;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HighlightLogsExpressionAction {
|
||||
type: ActionTypes.HighlightLogsExpression;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
expressions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitializeExploreAction {
|
||||
type: ActionTypes.InitializeExplore;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
containerWidth: number;
|
||||
datasource: string;
|
||||
eventBridge: Emitter;
|
||||
exploreDatasources: DataSourceSelectItem[];
|
||||
queries: DataQuery[];
|
||||
range: RawTimeRange;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitializeExploreSplitAction {
|
||||
type: ActionTypes.InitializeExploreSplit;
|
||||
}
|
||||
|
||||
export interface LoadDatasourceFailureAction {
|
||||
type: ActionTypes.LoadDatasourceFailure;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoadDatasourcePendingAction {
|
||||
type: ActionTypes.LoadDatasourcePending;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
datasourceId: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoadDatasourceMissingAction {
|
||||
type: ActionTypes.LoadDatasourceMissing;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoadDatasourceSuccessAction {
|
||||
type: ActionTypes.LoadDatasourceSuccess;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
StartPage?: any;
|
||||
datasourceInstance: any;
|
||||
history: HistoryItem[];
|
||||
initialDatasource: string;
|
||||
initialQueries: DataQuery[];
|
||||
logsHighlighterExpressions?: any[];
|
||||
showingStartPage: boolean;
|
||||
supportsGraph: boolean;
|
||||
supportsLogs: boolean;
|
||||
supportsTable: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModifyQueriesAction {
|
||||
type: ActionTypes.ModifyQueries;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
modification: any;
|
||||
index: number;
|
||||
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface QueryTransactionFailureAction {
|
||||
type: ActionTypes.QueryTransactionFailure;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
queryTransactions: QueryTransaction[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface QueryTransactionStartAction {
|
||||
type: ActionTypes.QueryTransactionStart;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
resultType: ResultType;
|
||||
rowIndex: number;
|
||||
transaction: QueryTransaction;
|
||||
};
|
||||
}
|
||||
|
||||
export interface QueryTransactionSuccessAction {
|
||||
type: ActionTypes.QueryTransactionSuccess;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
history: HistoryItem[];
|
||||
queryTransactions: QueryTransaction[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveQueryRowAction {
|
||||
type: ActionTypes.RemoveQueryRow;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
index: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RunQueriesEmptyAction {
|
||||
type: ActionTypes.RunQueriesEmpty;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScanStartAction {
|
||||
type: ActionTypes.ScanStart;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
scanner: RangeScanner;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScanRangeAction {
|
||||
type: ActionTypes.ScanRange;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
range: RawTimeRange;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScanStopAction {
|
||||
type: ActionTypes.ScanStop;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetQueriesAction {
|
||||
type: ActionTypes.SetQueries;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
queries: DataQuery[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SplitCloseAction {
|
||||
type: ActionTypes.SplitClose;
|
||||
}
|
||||
|
||||
export interface SplitOpenAction {
|
||||
type: ActionTypes.SplitOpen;
|
||||
payload: {
|
||||
itemState: ExploreItemState;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StateSaveAction {
|
||||
type: ActionTypes.StateSave;
|
||||
}
|
||||
|
||||
export interface ToggleTableAction {
|
||||
type: ActionTypes.ToggleTable;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToggleGraphAction {
|
||||
type: ActionTypes.ToggleGraph;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToggleLogsAction {
|
||||
type: ActionTypes.ToggleLogs;
|
||||
payload: {
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| AddQueryRowAction
|
||||
| ChangeQueryAction
|
||||
| ChangeSizeAction
|
||||
| ChangeTimeAction
|
||||
| ClearQueriesAction
|
||||
| HighlightLogsExpressionAction
|
||||
| InitializeExploreAction
|
||||
| InitializeExploreSplitAction
|
||||
| LoadDatasourceFailureAction
|
||||
| LoadDatasourceMissingAction
|
||||
| LoadDatasourcePendingAction
|
||||
| LoadDatasourceSuccessAction
|
||||
| ModifyQueriesAction
|
||||
| QueryTransactionFailureAction
|
||||
| QueryTransactionStartAction
|
||||
| QueryTransactionSuccessAction
|
||||
| RemoveQueryRowAction
|
||||
| RunQueriesEmptyAction
|
||||
| ScanRangeAction
|
||||
| ScanStartAction
|
||||
| ScanStopAction
|
||||
| SetQueriesAction
|
||||
| SplitCloseAction
|
||||
| SplitOpenAction
|
||||
| ToggleGraphAction
|
||||
| ToggleLogsAction
|
||||
| ToggleTableAction;
|
||||
757
public/app/features/explore/state/actions.ts
Normal file
757
public/app/features/explore/state/actions.ts
Normal file
@@ -0,0 +1,757 @@
|
||||
import _ from 'lodash';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
LAST_USED_DATASOURCE_KEY,
|
||||
clearQueryKeys,
|
||||
ensureQueries,
|
||||
generateEmptyQuery,
|
||||
hasNonEmptyQuery,
|
||||
makeTimeSeriesList,
|
||||
updateHistory,
|
||||
buildQueryTransaction,
|
||||
serializeStateToUrlParam,
|
||||
} from 'app/core/utils/explore';
|
||||
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import store from 'app/core/store';
|
||||
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||
import { DataQuery, StoreState } from 'app/types';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import {
|
||||
ExploreId,
|
||||
ExploreUrlState,
|
||||
RangeScanner,
|
||||
ResultType,
|
||||
QueryOptions,
|
||||
QueryTransaction,
|
||||
QueryHint,
|
||||
QueryHintGetter,
|
||||
} from 'app/types/explore';
|
||||
import { Emitter } from 'app/core/core';
|
||||
|
||||
import {
|
||||
Action as ThunkableAction,
|
||||
ActionTypes,
|
||||
AddQueryRowAction,
|
||||
ChangeSizeAction,
|
||||
HighlightLogsExpressionAction,
|
||||
LoadDatasourceFailureAction,
|
||||
LoadDatasourceMissingAction,
|
||||
LoadDatasourcePendingAction,
|
||||
LoadDatasourceSuccessAction,
|
||||
QueryTransactionStartAction,
|
||||
ScanStopAction,
|
||||
} from './actionTypes';
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
|
||||
|
||||
/**
|
||||
* Adds a query row after the row with the given index.
|
||||
*/
|
||||
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
|
||||
const query = generateEmptyQuery(index + 1);
|
||||
return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new datasource identified by the given name.
|
||||
*/
|
||||
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const instance = await getDatasourceSrv().get(datasource);
|
||||
dispatch(loadDatasource(exploreId, instance));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Query change handler for the query row with the given index.
|
||||
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
|
||||
*/
|
||||
export function changeQuery(
|
||||
exploreId: ExploreId,
|
||||
query: DataQuery,
|
||||
index: number,
|
||||
override: boolean
|
||||
): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
// Null query means reset
|
||||
if (query === null) {
|
||||
query = { ...generateEmptyQuery(index) };
|
||||
}
|
||||
|
||||
dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
|
||||
if (override) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of the Explore container size, in particular the width.
|
||||
* The width will be used to calculate graph intervals (number of datapoints).
|
||||
*/
|
||||
export function changeSize(
|
||||
exploreId: ExploreId,
|
||||
{ height, width }: { height: number; width: number }
|
||||
): ChangeSizeAction {
|
||||
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
||||
*/
|
||||
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queries and results.
|
||||
*/
|
||||
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch(scanStop(exploreId));
|
||||
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight expressions in the log results
|
||||
*/
|
||||
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
|
||||
return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Explore state with state from the URL and the React component.
|
||||
* Call this only on components for with the Explore state has not been initialized.
|
||||
*/
|
||||
export function initializeExplore(
|
||||
exploreId: ExploreId,
|
||||
datasource: string,
|
||||
queries: DataQuery[],
|
||||
range: RawTimeRange,
|
||||
containerWidth: number,
|
||||
eventBridge: Emitter
|
||||
): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||
.getExternal()
|
||||
.map(ds => ({
|
||||
value: ds.name,
|
||||
name: ds.name,
|
||||
meta: ds.meta,
|
||||
}));
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.InitializeExplore,
|
||||
payload: {
|
||||
exploreId,
|
||||
containerWidth,
|
||||
datasource,
|
||||
eventBridge,
|
||||
exploreDatasources,
|
||||
queries,
|
||||
range,
|
||||
},
|
||||
});
|
||||
|
||||
if (exploreDatasources.length > 1) {
|
||||
let instance;
|
||||
if (datasource) {
|
||||
instance = await getDatasourceSrv().get(datasource);
|
||||
} else {
|
||||
instance = await getDatasourceSrv().get();
|
||||
}
|
||||
dispatch(loadDatasource(exploreId, instance));
|
||||
} else {
|
||||
dispatch(loadDatasourceMissing(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the wrapper split state
|
||||
*/
|
||||
export function initializeExploreSplit() {
|
||||
return async dispatch => {
|
||||
dispatch({ type: ActionTypes.InitializeExploreSplit });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error that happened during the selection of a datasource
|
||||
*/
|
||||
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
|
||||
type: ActionTypes.LoadDatasourceFailure,
|
||||
payload: {
|
||||
exploreId,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Display an error when no datasources have been configured
|
||||
*/
|
||||
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
|
||||
type: ActionTypes.LoadDatasourceMissing,
|
||||
payload: { exploreId },
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the async process of loading a datasource to display a loading indicator
|
||||
*/
|
||||
export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
|
||||
type: ActionTypes.LoadDatasourcePending,
|
||||
payload: {
|
||||
exploreId,
|
||||
datasourceId,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
|
||||
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
||||
* e.g., Prometheus -> Loki queries.
|
||||
*/
|
||||
export const loadDatasourceSuccess = (
|
||||
exploreId: ExploreId,
|
||||
instance: any,
|
||||
queries: DataQuery[]
|
||||
): LoadDatasourceSuccessAction => {
|
||||
// Capabilities
|
||||
const supportsGraph = instance.meta.metrics;
|
||||
const supportsLogs = instance.meta.logs;
|
||||
const supportsTable = instance.meta.tables;
|
||||
// Custom components
|
||||
const StartPage = instance.pluginExports.ExploreStartPage;
|
||||
|
||||
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
// Save last-used datasource
|
||||
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
|
||||
|
||||
return {
|
||||
type: ActionTypes.LoadDatasourceSuccess,
|
||||
payload: {
|
||||
exploreId,
|
||||
StartPage,
|
||||
datasourceInstance: instance,
|
||||
history,
|
||||
initialDatasource: instance.name,
|
||||
initialQueries: queries,
|
||||
showingStartPage: Boolean(StartPage),
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
|
||||
*/
|
||||
export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
const datasourceId = instance.meta.id;
|
||||
|
||||
// Keep ID to track selection
|
||||
dispatch(loadDatasourcePending(exploreId, datasourceId));
|
||||
|
||||
let datasourceError = null;
|
||||
try {
|
||||
const testResult = await instance.testDatasource();
|
||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||
} catch (error) {
|
||||
datasourceError = (error && error.statusText) || 'Network error';
|
||||
}
|
||||
if (datasourceError) {
|
||||
dispatch(loadDatasourceFailure(exploreId, datasourceError));
|
||||
return;
|
||||
}
|
||||
|
||||
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
|
||||
// User already changed datasource again, discard results
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.init) {
|
||||
instance.init();
|
||||
}
|
||||
|
||||
// Check if queries can be imported from previously selected datasource
|
||||
const queries = getState().explore[exploreId].modifiedQueries;
|
||||
let importedQueries = queries;
|
||||
const origin = getState().explore[exploreId].datasourceInstance;
|
||||
if (origin) {
|
||||
if (origin.meta.id === instance.meta.id) {
|
||||
// Keep same queries if same type of datasource
|
||||
importedQueries = [...queries];
|
||||
} else if (instance.importQueries) {
|
||||
// Datasource-specific importers
|
||||
importedQueries = await instance.importQueries(queries, origin.meta);
|
||||
} else {
|
||||
// Default is blank queries
|
||||
importedQueries = ensureQueries();
|
||||
}
|
||||
}
|
||||
|
||||
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
|
||||
// User already changed datasource again, discard results
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset edit state with new queries
|
||||
const nextQueries = importedQueries.map((q, i) => ({
|
||||
...importedQueries[i],
|
||||
...generateEmptyQuery(i),
|
||||
}));
|
||||
|
||||
dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to modify a query given a datasource-specific modifier action.
|
||||
* @param exploreId Explore area
|
||||
* @param modification Action object with a type, e.g., ADD_FILTER
|
||||
* @param index Optional query row index. If omitted, the modification is applied to all query rows.
|
||||
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
||||
*/
|
||||
export function modifyQueries(
|
||||
exploreId: ExploreId,
|
||||
modification: any,
|
||||
index: number,
|
||||
modifier: any
|
||||
): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
|
||||
if (!modification.preventSubmit) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a query transaction as failed with an error extracted from the query response.
|
||||
* The transaction will be marked as `done`.
|
||||
*/
|
||||
export function queryTransactionFailure(
|
||||
exploreId: ExploreId,
|
||||
transactionId: string,
|
||||
response: any,
|
||||
datasourceId: string
|
||||
): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
|
||||
if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
|
||||
// Navigated away, queries did not matter
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction might have been discarded
|
||||
if (!queryTransactions.find(qt => qt.id === transactionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(response);
|
||||
|
||||
let error: string;
|
||||
let errorDetails: string;
|
||||
if (response.data) {
|
||||
if (typeof response.data === 'string') {
|
||||
error = response.data;
|
||||
} else if (response.data.error) {
|
||||
error = response.data.error;
|
||||
if (response.data.response) {
|
||||
errorDetails = response.data.response;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not handle error response');
|
||||
}
|
||||
} else if (response.message) {
|
||||
error = response.message;
|
||||
} else if (typeof response === 'string') {
|
||||
error = response;
|
||||
} else {
|
||||
error = 'Unknown error during query transaction. Please check JS console logs.';
|
||||
}
|
||||
|
||||
// Mark transactions as complete
|
||||
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||
if (qt.id === transactionId) {
|
||||
return {
|
||||
...qt,
|
||||
error,
|
||||
errorDetails,
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
return qt;
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.QueryTransactionFailure,
|
||||
payload: { exploreId, queryTransactions: nextQueryTransactions },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a query transaction for the given result type.
|
||||
* @param exploreId Explore area
|
||||
* @param transaction Query options and `done` status.
|
||||
* @param resultType Associate the transaction with a result viewer, e.g., Graph
|
||||
* @param rowIndex Index is used to associate latency for this transaction with a query row
|
||||
*/
|
||||
export function queryTransactionStart(
|
||||
exploreId: ExploreId,
|
||||
transaction: QueryTransaction,
|
||||
resultType: ResultType,
|
||||
rowIndex: number
|
||||
): QueryTransactionStartAction {
|
||||
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
|
||||
* If the transaction was started by a scanner, it keeps on scanning for more results.
|
||||
* Side-effect: the query is stored in localStorage.
|
||||
* @param exploreId Explore area
|
||||
* @param transactionId ID
|
||||
* @param result Response from `datasourceInstance.query()`
|
||||
* @param latency Duration between request and response
|
||||
* @param queries Queries from all query rows
|
||||
* @param datasourceId Origin datasource instance, used to discard results if current datasource is different
|
||||
*/
|
||||
export function queryTransactionSuccess(
|
||||
exploreId: ExploreId,
|
||||
transactionId: string,
|
||||
result: any,
|
||||
latency: number,
|
||||
queries: DataQuery[],
|
||||
datasourceId: string
|
||||
): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
|
||||
|
||||
// If datasource already changed, results do not matter
|
||||
if (datasourceInstance.meta.id !== datasourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction might have been discarded
|
||||
const transaction = queryTransactions.find(qt => qt.id === transactionId);
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get query hints
|
||||
let hints: QueryHint[];
|
||||
if (datasourceInstance.getQueryHints as QueryHintGetter) {
|
||||
hints = datasourceInstance.getQueryHints(transaction.query, result);
|
||||
}
|
||||
|
||||
// Mark transactions as complete and attach result
|
||||
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||
if (qt.id === transactionId) {
|
||||
return {
|
||||
...qt,
|
||||
hints,
|
||||
latency,
|
||||
result,
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
return qt;
|
||||
});
|
||||
|
||||
// Side-effect: Saving history in localstorage
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.QueryTransactionSuccess,
|
||||
payload: {
|
||||
exploreId,
|
||||
history: nextHistory,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
},
|
||||
});
|
||||
|
||||
// Keep scanning for results if this was the last scanning transaction
|
||||
if (scanning) {
|
||||
if (_.size(result) === 0) {
|
||||
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
|
||||
if (!other) {
|
||||
const range = scanner();
|
||||
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||
}
|
||||
} else {
|
||||
// We can stop scanning if we have a result
|
||||
dispatch(scanStop(exploreId));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query row of the given index, as well as associated query results.
|
||||
*/
|
||||
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||
*/
|
||||
export function runQueries(exploreId: ExploreId) {
|
||||
return (dispatch, getState) => {
|
||||
const {
|
||||
datasourceInstance,
|
||||
modifiedQueries,
|
||||
showingLogs,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
} = getState().explore[exploreId];
|
||||
|
||||
if (!hasNonEmptyQuery(modifiedQueries)) {
|
||||
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
|
||||
return;
|
||||
}
|
||||
|
||||
// Some datasource's query builders allow per-query interval limits,
|
||||
// but we're using the datasource interval limit for now
|
||||
const interval = datasourceInstance.interval;
|
||||
|
||||
// Keep table queries first since they need to return quickly
|
||||
if (showingTable && supportsTable) {
|
||||
dispatch(
|
||||
runQueriesForType(
|
||||
exploreId,
|
||||
'Table',
|
||||
{
|
||||
interval,
|
||||
format: 'table',
|
||||
instant: true,
|
||||
valueWithRefId: true,
|
||||
},
|
||||
data => data[0]
|
||||
)
|
||||
);
|
||||
}
|
||||
if (showingGraph && supportsGraph) {
|
||||
dispatch(
|
||||
runQueriesForType(
|
||||
exploreId,
|
||||
'Graph',
|
||||
{
|
||||
interval,
|
||||
format: 'time_series',
|
||||
instant: false,
|
||||
},
|
||||
makeTimeSeriesList
|
||||
)
|
||||
);
|
||||
}
|
||||
if (showingLogs && supportsLogs) {
|
||||
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
||||
}
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper action to build a query transaction object and handing the query to the datasource.
|
||||
* @param exploreId Explore area
|
||||
* @param resultType Result viewer that will be associated with this query result
|
||||
* @param queryOptions Query options as required by the datasource's `query()` function.
|
||||
* @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
|
||||
*/
|
||||
function runQueriesForType(
|
||||
exploreId: ExploreId,
|
||||
resultType: ResultType,
|
||||
queryOptions: QueryOptions,
|
||||
resultGetter?: any
|
||||
) {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
datasourceInstance,
|
||||
eventBridge,
|
||||
modifiedQueries: queries,
|
||||
queryIntervals,
|
||||
range,
|
||||
scanning,
|
||||
} = getState().explore[exploreId];
|
||||
const datasourceId = datasourceInstance.meta.id;
|
||||
|
||||
// Run all queries concurrently
|
||||
queries.forEach(async (query, rowIndex) => {
|
||||
const transaction = buildQueryTransaction(
|
||||
query,
|
||||
rowIndex,
|
||||
resultType,
|
||||
queryOptions,
|
||||
range,
|
||||
queryIntervals,
|
||||
scanning
|
||||
);
|
||||
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
|
||||
try {
|
||||
const now = Date.now();
|
||||
const res = await datasourceInstance.query(transaction.options);
|
||||
eventBridge.emit('data-received', res.data || []);
|
||||
const latency = Date.now() - now;
|
||||
const results = resultGetter ? resultGetter(res.data) : res.data;
|
||||
dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
|
||||
} catch (response) {
|
||||
eventBridge.emit('data-error', response);
|
||||
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a scan for more results using the given scanner.
|
||||
* @param exploreId Explore area
|
||||
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
||||
*/
|
||||
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
// Register the scanner
|
||||
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
|
||||
// Scanning must trigger query run, and return the new range
|
||||
const range = scanner();
|
||||
// Set the new range to be displayed
|
||||
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any scanning for more results.
|
||||
*/
|
||||
export function scanStop(exploreId: ExploreId): ScanStopAction {
|
||||
return { type: ActionTypes.ScanStop, payload: { exploreId } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset queries to the given queries. Any modifications will be discarded.
|
||||
* Use this action for clicks on query examples. Triggers a query run.
|
||||
*/
|
||||
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
// Inject react keys into query objects
|
||||
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
|
||||
dispatch({
|
||||
type: ActionTypes.SetQueries,
|
||||
payload: {
|
||||
exploreId,
|
||||
queries,
|
||||
},
|
||||
});
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the split view and save URL state.
|
||||
*/
|
||||
export function splitClose(): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.SplitClose });
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the split view and copy the left state to be the right state.
|
||||
* The right state is automatically initialized.
|
||||
* The copy keeps all query modifications but wipes the query results.
|
||||
*/
|
||||
export function splitOpen(): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
// Clone left state to become the right state
|
||||
const leftState = getState().explore.left;
|
||||
const itemState = {
|
||||
...leftState,
|
||||
queryTransactions: [],
|
||||
initialQueries: leftState.modifiedQueries.slice(),
|
||||
};
|
||||
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves Explore state to URL using the `left` and `right` parameters.
|
||||
* If split view is not active, `right` will not be set.
|
||||
*/
|
||||
export function stateSave() {
|
||||
return (dispatch, getState) => {
|
||||
const { left, right, split } = getState().explore;
|
||||
const urlStates: { [index: string]: string } = {};
|
||||
const leftUrlState: ExploreUrlState = {
|
||||
datasource: left.datasourceInstance.name,
|
||||
queries: left.modifiedQueries.map(clearQueryKeys),
|
||||
range: left.range,
|
||||
};
|
||||
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||
if (split) {
|
||||
const rightUrlState: ExploreUrlState = {
|
||||
datasource: right.datasourceInstance.name,
|
||||
queries: right.modifiedQueries.map(clearQueryKeys),
|
||||
range: right.range,
|
||||
};
|
||||
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||
}
|
||||
dispatch(updateLocation({ query: urlStates }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||
*/
|
||||
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
|
||||
if (getState().explore[exploreId].showingGraph) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
||||
*/
|
||||
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
|
||||
if (getState().explore[exploreId].showingLogs) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
||||
*/
|
||||
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
|
||||
if (getState().explore[exploreId].showingTable) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
462
public/app/features/explore/state/reducers.ts
Normal file
462
public/app/features/explore/state/reducers.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import {
|
||||
calculateResultsFromQueryTransactions,
|
||||
generateEmptyQuery,
|
||||
getIntervals,
|
||||
ensureQueries,
|
||||
} from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
|
||||
import { DataQuery } from 'app/types/series';
|
||||
|
||||
import { Action, ActionTypes } from './actionTypes';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
// Millies step for helper bar charts
|
||||
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
||||
|
||||
/**
|
||||
* Returns a fresh Explore area state
|
||||
*/
|
||||
const makeExploreItemState = (): ExploreItemState => ({
|
||||
StartPage: undefined,
|
||||
containerWidth: 0,
|
||||
datasourceInstance: null,
|
||||
datasourceError: null,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
exploreDatasources: [],
|
||||
history: [],
|
||||
initialQueries: [],
|
||||
initialized: false,
|
||||
modifiedQueries: [],
|
||||
queryTransactions: [],
|
||||
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
||||
range: DEFAULT_RANGE,
|
||||
scanning: false,
|
||||
scanRange: null,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Global Explore state that handles multiple Explore areas and the split state
|
||||
*/
|
||||
const initialExploreState: ExploreState = {
|
||||
split: null,
|
||||
left: makeExploreItemState(),
|
||||
right: makeExploreItemState(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for an Explore area, to be used by the global Explore reducer.
|
||||
*/
|
||||
const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddQueryRow: {
|
||||
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||
const { index, query } = action.payload;
|
||||
|
||||
// Add new query row after given index, keep modifications of existing rows
|
||||
const nextModifiedQueries = [
|
||||
...modifiedQueries.slice(0, index + 1),
|
||||
{ ...query },
|
||||
...initialQueries.slice(index + 1),
|
||||
];
|
||||
|
||||
// Add to initialQueries, which will cause a new row to be rendered
|
||||
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
|
||||
|
||||
// Ongoing transactions need to update their row indices
|
||||
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||
if (qt.rowIndex > index) {
|
||||
return {
|
||||
...qt,
|
||||
rowIndex: qt.rowIndex + 1,
|
||||
};
|
||||
}
|
||||
return qt;
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
logsHighlighterExpressions: undefined,
|
||||
modifiedQueries: nextModifiedQueries,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeQuery: {
|
||||
const { initialQueries, queryTransactions } = state;
|
||||
let { modifiedQueries } = state;
|
||||
const { query, index, override } = action.payload;
|
||||
|
||||
// Fast path: only change modifiedQueries to not trigger an update
|
||||
modifiedQueries[index] = query;
|
||||
if (!override) {
|
||||
return {
|
||||
...state,
|
||||
modifiedQueries,
|
||||
};
|
||||
}
|
||||
|
||||
// Override path: queries are completely reset
|
||||
const nextQuery: DataQuery = {
|
||||
...query,
|
||||
...generateEmptyQuery(index),
|
||||
};
|
||||
const nextQueries = [...initialQueries];
|
||||
nextQueries[index] = nextQuery;
|
||||
modifiedQueries = [...nextQueries];
|
||||
|
||||
// Discard ongoing transaction related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeSize: {
|
||||
const { range, datasourceInstance } = state;
|
||||
let interval = '1s';
|
||||
if (datasourceInstance && datasourceInstance.interval) {
|
||||
interval = datasourceInstance.interval;
|
||||
}
|
||||
const containerWidth = action.payload.width;
|
||||
const queryIntervals = getIntervals(range, interval, containerWidth);
|
||||
return { ...state, containerWidth, queryIntervals };
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeTime: {
|
||||
return {
|
||||
...state,
|
||||
range: action.payload.range,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ClearQueries: {
|
||||
const queries = ensureQueries();
|
||||
return {
|
||||
...state,
|
||||
initialQueries: queries.slice(),
|
||||
modifiedQueries: queries.slice(),
|
||||
queryTransactions: [],
|
||||
showingStartPage: Boolean(state.StartPage),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.HighlightLogsExpression: {
|
||||
const { expressions } = action.payload;
|
||||
return { ...state, logsHighlighterExpressions: expressions };
|
||||
}
|
||||
|
||||
case ActionTypes.InitializeExplore: {
|
||||
const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
containerWidth,
|
||||
eventBridge,
|
||||
exploreDatasources,
|
||||
range,
|
||||
initialDatasource: datasource,
|
||||
initialQueries: queries,
|
||||
initialized: true,
|
||||
modifiedQueries: queries.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceFailure: {
|
||||
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceMissing: {
|
||||
return { ...state, datasourceMissing: true, datasourceLoading: false };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourcePending: {
|
||||
return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceSuccess: {
|
||||
const { containerWidth, range } = state;
|
||||
const {
|
||||
StartPage,
|
||||
datasourceInstance,
|
||||
history,
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
showingStartPage,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
} = action.payload;
|
||||
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
|
||||
|
||||
return {
|
||||
...state,
|
||||
queryIntervals,
|
||||
StartPage,
|
||||
datasourceInstance,
|
||||
history,
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
showingStartPage,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
datasourceLoading: false,
|
||||
datasourceMissing: false,
|
||||
logsHighlighterExpressions: undefined,
|
||||
modifiedQueries: initialQueries.slice(),
|
||||
queryTransactions: [],
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ModifyQueries: {
|
||||
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||
const { modification, index, modifier } = action.payload as any;
|
||||
let nextQueries: DataQuery[];
|
||||
let nextQueryTransactions;
|
||||
if (index === undefined) {
|
||||
// Modify all queries
|
||||
nextQueries = initialQueries.map((query, i) => ({
|
||||
...modifier(modifiedQueries[i], modification),
|
||||
...generateEmptyQuery(i),
|
||||
}));
|
||||
// Discard all ongoing transactions
|
||||
nextQueryTransactions = [];
|
||||
} else {
|
||||
// Modify query only at index
|
||||
nextQueries = initialQueries.map((query, i) => {
|
||||
// Synchronize all queries with local query cache to ensure consistency
|
||||
// TODO still needed?
|
||||
return i === index
|
||||
? {
|
||||
...modifier(modifiedQueries[i], modification),
|
||||
...generateEmptyQuery(i),
|
||||
}
|
||||
: query;
|
||||
});
|
||||
nextQueryTransactions = queryTransactions
|
||||
// Consume the hint corresponding to the action
|
||||
.map(qt => {
|
||||
if (qt.hints != null && qt.rowIndex === index) {
|
||||
qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
|
||||
}
|
||||
return qt;
|
||||
})
|
||||
// Preserve previous row query transaction to keep results visible if next query is incomplete
|
||||
.filter(qt => modification.preventSubmit || qt.rowIndex !== index);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionFailure: {
|
||||
const { queryTransactions } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
queryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionStart: {
|
||||
const { datasourceInstance, queryIntervals, queryTransactions } = state;
|
||||
const { resultType, rowIndex, transaction } = action.payload;
|
||||
// Discarding existing transactions of same type
|
||||
const remainingTransactions = queryTransactions.filter(
|
||||
qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
|
||||
);
|
||||
|
||||
// Append new transaction
|
||||
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
|
||||
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
nextQueryTransactions,
|
||||
datasourceInstance,
|
||||
queryIntervals.intervalMs
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...results,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionSuccess: {
|
||||
const { datasourceInstance, queryIntervals } = state;
|
||||
const { history, queryTransactions } = action.payload;
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
queryTransactions,
|
||||
datasourceInstance,
|
||||
queryIntervals.intervalMs
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...results,
|
||||
history,
|
||||
queryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.RemoveQueryRow: {
|
||||
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
|
||||
let { modifiedQueries } = state;
|
||||
const { index } = action.payload;
|
||||
|
||||
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
|
||||
|
||||
if (initialQueries.length <= 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
|
||||
|
||||
// Discard transactions related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
nextQueryTransactions,
|
||||
datasourceInstance,
|
||||
queryIntervals.intervalMs
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...results,
|
||||
initialQueries: nextQueries,
|
||||
logsHighlighterExpressions: undefined,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.RunQueriesEmpty: {
|
||||
return { ...state, queryTransactions: [] };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanRange: {
|
||||
return { ...state, scanRange: action.payload.range };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStart: {
|
||||
return { ...state, scanning: true };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStop: {
|
||||
const { queryTransactions } = state;
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
|
||||
return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
|
||||
}
|
||||
|
||||
case ActionTypes.SetQueries: {
|
||||
const { queries } = action.payload;
|
||||
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleGraph: {
|
||||
const showingGraph = !state.showingGraph;
|
||||
let nextQueryTransactions = state.queryTransactions;
|
||||
if (!showingGraph) {
|
||||
// Discard transactions related to Graph query
|
||||
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
|
||||
}
|
||||
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleLogs: {
|
||||
const showingLogs = !state.showingLogs;
|
||||
let nextQueryTransactions = state.queryTransactions;
|
||||
if (!showingLogs) {
|
||||
// Discard transactions related to Logs query
|
||||
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
|
||||
}
|
||||
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleTable: {
|
||||
const showingTable = !state.showingTable;
|
||||
if (showingTable) {
|
||||
return { ...state, showingTable, queryTransactions: state.queryTransactions };
|
||||
}
|
||||
|
||||
// Toggle off needs discarding of table queries and results
|
||||
const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
nextQueryTransactions,
|
||||
state.datasourceInstance,
|
||||
state.queryIntervals.intervalMs
|
||||
);
|
||||
|
||||
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global Explore reducer that handles multiple Explore areas (left and right).
|
||||
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
|
||||
*/
|
||||
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SplitClose: {
|
||||
return {
|
||||
...state,
|
||||
split: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.SplitOpen: {
|
||||
return {
|
||||
...state,
|
||||
split: true,
|
||||
right: action.payload.itemState,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.InitializeExploreSplit: {
|
||||
return {
|
||||
...state,
|
||||
split: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (action.payload) {
|
||||
const { exploreId } = action.payload as any;
|
||||
if (exploreId !== undefined) {
|
||||
const exploreItemState = state[exploreId];
|
||||
return {
|
||||
...state,
|
||||
[exploreId]: itemReducer(exploreItemState, action),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
explore: exploreReducer,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Label } from '@grafana/ui';
|
||||
|
||||
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import { updateTeam } from './state/actions';
|
||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||
|
||||
@@ -132,7 +132,7 @@ export class VariableSrv {
|
||||
|
||||
return this.$q.all(promises).then(() => {
|
||||
if (emitChangeEvents) {
|
||||
this.$rootScope.$emit('template-variable-value-updated');
|
||||
this.$rootScope.appEvent('template-variable-value-updated');
|
||||
this.dashboard.startRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
|
||||
import { FormField, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
|
||||
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
import { FormField } from '@grafana/ui/src';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
|
||||
onToggleThresholdLabels = () =>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GaugeOptions, PanelProps, NullValueMode } from '@grafana/ui';
|
||||
import { PanelProps, NullValueMode } from '@grafana/ui';
|
||||
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
interface Props extends PanelProps<GaugeOptions> {}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { timeSeries, width, height } = this.props;
|
||||
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
||||
|
||||
const prefix = onInterpolate(options.prefix);
|
||||
const suffix = onInterpolate(options.suffix);
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
@@ -21,6 +25,8 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
{...this.props.options}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
BasicGaugeColor,
|
||||
GaugeOptions,
|
||||
PanelOptionsProps,
|
||||
ThresholdsEditor,
|
||||
Threshold,
|
||||
PanelOptionsGrid,
|
||||
ValueMappingsEditor,
|
||||
ValueMapping,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
|
||||
import ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
|
||||
import GaugeOptionsEditor from './GaugeOptionsEditor';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
@@ -24,7 +25,7 @@ export const defaultProps = {
|
||||
decimals: 0,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
mappings: [],
|
||||
valueMappings: [],
|
||||
thresholds: [],
|
||||
},
|
||||
};
|
||||
@@ -32,7 +33,17 @@ export const defaultProps = {
|
||||
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds });
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
@@ -44,7 +55,7 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappings onChange={onChange} options={options} />
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormField, Label, GaugeOptions, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
|
||||
import { FormField, Label, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
|
||||
import UnitPicker from 'app/core/components/Select/UnitPicker';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
const statOptions = [
|
||||
{ value: 'min', label: 'Min' },
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||
|
||||
|
||||
export interface GaugeOptions {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers';
|
||||
import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
||||
import foldersReducers from 'app/features/folders/state/reducers';
|
||||
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||
import exploreReducers from 'app/features/explore/state/reducers';
|
||||
import pluginReducers from 'app/features/plugins/state/reducers';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
import usersReducers from 'app/features/users/state/reducers';
|
||||
@@ -20,6 +21,7 @@ const rootReducers = {
|
||||
...apiKeysReducers,
|
||||
...foldersReducers,
|
||||
...dashboardReducers,
|
||||
...exploreReducers,
|
||||
...pluginReducers,
|
||||
...dataSourcesReducers,
|
||||
...usersReducers,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Value } from 'slate';
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { LogsModel } from 'app/core/logs_model';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||
|
||||
import { DataQuery } from './series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { LogsModel } from 'app/core/logs_model';
|
||||
import { DataSourceSelectItem } from 'app/types/datasources';
|
||||
|
||||
export interface CompletionItem {
|
||||
/**
|
||||
@@ -76,6 +78,174 @@ export interface CompletionItemGroup {
|
||||
skipSort?: boolean;
|
||||
}
|
||||
|
||||
export enum ExploreId {
|
||||
left = 'left',
|
||||
right = 'right',
|
||||
}
|
||||
|
||||
/**
|
||||
* Global Explore state
|
||||
*/
|
||||
export interface ExploreState {
|
||||
/**
|
||||
* True if split view is active.
|
||||
*/
|
||||
split: boolean;
|
||||
/**
|
||||
* Explore state of the left split (left is default in non-split view).
|
||||
*/
|
||||
left: ExploreItemState;
|
||||
/**
|
||||
* Explore state of the right area in split view.
|
||||
*/
|
||||
right: ExploreItemState;
|
||||
}
|
||||
|
||||
export interface ExploreItemState {
|
||||
/**
|
||||
* React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
|
||||
*/
|
||||
StartPage?: any;
|
||||
/**
|
||||
* Width used for calculating the graph interval (can't have more datapoints than pixels)
|
||||
*/
|
||||
containerWidth: number;
|
||||
/**
|
||||
* Datasource instance that has been selected. Datasource-specific logic can be run on this object.
|
||||
*/
|
||||
datasourceInstance: any;
|
||||
/**
|
||||
* Error to be shown when datasource loading or testing failed.
|
||||
*/
|
||||
datasourceError: string;
|
||||
/**
|
||||
* True if the datasource is loading. `null` if the loading has not started yet.
|
||||
*/
|
||||
datasourceLoading: boolean | null;
|
||||
/**
|
||||
* True if there is no datasource to be selected.
|
||||
*/
|
||||
datasourceMissing: boolean;
|
||||
/**
|
||||
* Emitter to send events to the rest of Grafana.
|
||||
*/
|
||||
eventBridge?: Emitter;
|
||||
/**
|
||||
* List of datasources to be shown in the datasource selector.
|
||||
*/
|
||||
exploreDatasources: DataSourceSelectItem[];
|
||||
/**
|
||||
* List of timeseries to be shown in the Explore graph result viewer.
|
||||
*/
|
||||
graphResult?: any[];
|
||||
/**
|
||||
* History of recent queries. Datasource-specific and initialized via localStorage.
|
||||
*/
|
||||
history: HistoryItem[];
|
||||
/**
|
||||
* Initial datasource for this Explore, e.g., set via URL.
|
||||
*/
|
||||
initialDatasource?: string;
|
||||
/**
|
||||
* Initial queries for this Explore, e.g., set via URL. Each query will be
|
||||
* converted to a query row. Query edits should be tracked in `modifiedQueries` though.
|
||||
*/
|
||||
initialQueries: DataQuery[];
|
||||
/**
|
||||
* True if this Explore area has been initialized.
|
||||
* Used to distinguish URL state injection versus split view state injection.
|
||||
*/
|
||||
initialized: boolean;
|
||||
/**
|
||||
* Log line substrings to be highlighted as you type in a query field.
|
||||
* Currently supports only the first query row.
|
||||
*/
|
||||
logsHighlighterExpressions?: string[];
|
||||
/**
|
||||
* Log query result to be displayed in the logs result viewer.
|
||||
*/
|
||||
logsResult?: LogsModel;
|
||||
/**
|
||||
* Copy of `initialQueries` that tracks user edits.
|
||||
* Don't connect this property to a react component as it is updated on every query change.
|
||||
* Used when running queries. Needs to be reset to `initialQueries` when those are reset as well.
|
||||
*/
|
||||
modifiedQueries: DataQuery[];
|
||||
/**
|
||||
* Query intervals for graph queries to determine how many datapoints to return.
|
||||
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
|
||||
*/
|
||||
queryIntervals: QueryIntervals;
|
||||
/**
|
||||
* List of query transaction to track query duration and query result.
|
||||
* Graph/Logs/Table results are calculated on the fly from the transaction,
|
||||
* based on the transaction's result types. Transaction also holds the row index
|
||||
* so that results can be dropped and re-computed without running queries again
|
||||
* when query rows are removed.
|
||||
*/
|
||||
queryTransactions: QueryTransaction[];
|
||||
/**
|
||||
* Tracks datasource when selected in the datasource selector.
|
||||
* Allows the selection to be discarded if something went wrong during the asynchronous
|
||||
* loading of the datasource.
|
||||
*/
|
||||
requestedDatasourceId?: number;
|
||||
/**
|
||||
* Time range for this Explore. Managed by the time picker and used by all query runs.
|
||||
*/
|
||||
range: TimeRange | RawTimeRange;
|
||||
/**
|
||||
* Scanner function that calculates a new range, triggers a query run, and returns the new range.
|
||||
*/
|
||||
scanner?: RangeScanner;
|
||||
/**
|
||||
* True if scanning for more results is active.
|
||||
*/
|
||||
scanning?: boolean;
|
||||
/**
|
||||
* Current scanning range to be shown to the user while scanning is active.
|
||||
*/
|
||||
scanRange?: RawTimeRange;
|
||||
/**
|
||||
* True if graph result viewer is expanded. Query runs will contain graph queries.
|
||||
*/
|
||||
showingGraph: boolean;
|
||||
/**
|
||||
* True if logs result viewer is expanded. Query runs will contain logs queries.
|
||||
*/
|
||||
showingLogs: boolean;
|
||||
/**
|
||||
* True StartPage needs to be shown. Typically set to `false` once queries have been run.
|
||||
*/
|
||||
showingStartPage?: boolean;
|
||||
/**
|
||||
* True if table result viewer is expanded. Query runs will contain table queries.
|
||||
*/
|
||||
showingTable: boolean;
|
||||
/**
|
||||
* True if `datasourceInstance` supports graph queries.
|
||||
*/
|
||||
supportsGraph: boolean | null;
|
||||
/**
|
||||
* True if `datasourceInstance` supports logs queries.
|
||||
*/
|
||||
supportsLogs: boolean | null;
|
||||
/**
|
||||
* True if `datasourceInstance` supports table queries.
|
||||
*/
|
||||
supportsTable: boolean | null;
|
||||
/**
|
||||
* Table model that combines all query table results into a single table.
|
||||
*/
|
||||
tableResult?: TableModel;
|
||||
}
|
||||
|
||||
export interface ExploreUrlState {
|
||||
datasource: string;
|
||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||
range: RawTimeRange;
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
ts: number;
|
||||
query: DataQuery;
|
||||
@@ -128,6 +298,19 @@ export interface QueryHintGetter {
|
||||
(query: DataQuery, results: any[], ...rest: any): QueryHint[];
|
||||
}
|
||||
|
||||
export interface QueryIntervals {
|
||||
interval: string;
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
interval: string;
|
||||
format: string;
|
||||
hinting?: boolean;
|
||||
instant?: boolean;
|
||||
valueWithRefId?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryTransaction {
|
||||
id: string;
|
||||
done: boolean;
|
||||
@@ -142,6 +325,8 @@ export interface QueryTransaction {
|
||||
scanning?: boolean;
|
||||
}
|
||||
|
||||
export type RangeScanner = () => RawTimeRange;
|
||||
|
||||
export interface TextMatch {
|
||||
text: string;
|
||||
start: number;
|
||||
@@ -149,38 +334,4 @@ export interface TextMatch {
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface ExploreState {
|
||||
StartPage?: any;
|
||||
datasource: any;
|
||||
datasourceError: any;
|
||||
datasourceLoading: boolean | null;
|
||||
datasourceMissing: boolean;
|
||||
exploreDatasources: DataSourceSelectItem[];
|
||||
graphInterval: number; // in ms
|
||||
graphResult?: any[];
|
||||
history: HistoryItem[];
|
||||
initialDatasource?: string;
|
||||
initialQueries: DataQuery[];
|
||||
logsHighlighterExpressions?: string[];
|
||||
logsResult?: LogsModel;
|
||||
queryTransactions: QueryTransaction[];
|
||||
range: RawTimeRange;
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
showingGraph: boolean;
|
||||
showingLogs: boolean;
|
||||
showingStartPage?: boolean;
|
||||
showingTable: boolean;
|
||||
supportsGraph: boolean | null;
|
||||
supportsLogs: boolean | null;
|
||||
supportsTable: boolean | null;
|
||||
tableResult?: TableModel;
|
||||
}
|
||||
|
||||
export interface ExploreUrlState {
|
||||
datasource: string;
|
||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||
range: RawTimeRange;
|
||||
}
|
||||
|
||||
export type ResultType = 'Graph' | 'Logs' | 'Table';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from './appNotifications';
|
||||
import { DashboardSearchHit } from './search';
|
||||
import { ValidationEvents, ValidationRule } from './form';
|
||||
import { ExploreState } from './explore';
|
||||
export {
|
||||
Team,
|
||||
TeamsState,
|
||||
@@ -81,6 +82,7 @@ export interface StoreState {
|
||||
folder: FolderState;
|
||||
dashboard: DashboardState;
|
||||
dataSources: DataSourcesState;
|
||||
explore: ExploreState;
|
||||
users: UsersState;
|
||||
organization: OrganizationState;
|
||||
appNotifications: AppNotificationsState;
|
||||
|
||||
@@ -12,7 +12,7 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { BasicGaugeColor, Threshold, TimeSeriesVMs, RangeMap, ValueMap, MappingType } from '@grafana/ui';
|
||||
import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
|
||||
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
@@ -9,7 +9,7 @@ export interface Props {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
height: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
@@ -29,7 +29,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
@@ -64,25 +64,22 @@ export class Gauge extends PureComponent<Props> {
|
||||
}
|
||||
})[0];
|
||||
|
||||
return {
|
||||
rangeMap,
|
||||
valueMap,
|
||||
};
|
||||
return { rangeMap, valueMap };
|
||||
}
|
||||
|
||||
formatValue(value) {
|
||||
const { decimals, mappings, prefix, suffix, unit } = this.props;
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
const formatFunc = kbn.valueFormats[unit];
|
||||
const formattedValue = formatFunc(value, decimals);
|
||||
|
||||
if (mappings.length > 0) {
|
||||
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
|
||||
if (valueMappings.length > 0) {
|
||||
const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
|
||||
|
||||
if (valueMap) {
|
||||
return valueMap;
|
||||
return `${prefix} ${valueMap} ${suffix}`;
|
||||
} else if (rangeMap) {
|
||||
return rangeMap;
|
||||
return `${prefix} ${rangeMap} ${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +145,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
color: index === 0 ? threshold.color : thresholds[index].color,
|
||||
};
|
||||
}),
|
||||
{
|
||||
value: maxValue,
|
||||
color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
|
||||
},
|
||||
{ value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
|
||||
];
|
||||
|
||||
const options = {
|
||||
@@ -184,19 +178,14 @@ export class Gauge extends PureComponent<Props> {
|
||||
formatter: () => {
|
||||
return this.formatValue(value);
|
||||
},
|
||||
font: {
|
||||
size: fontSize,
|
||||
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
},
|
||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = {
|
||||
data: [[0, value]],
|
||||
};
|
||||
const plotSeries = { data: [[0, value]] };
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// DEPENDENCIES
|
||||
// DEPENDENCIES
|
||||
@import '../../node_modules/react-table/react-table.css';
|
||||
|
||||
// VENDOR
|
||||
@@ -97,7 +97,6 @@
|
||||
@import 'components/add_data_source.scss';
|
||||
@import 'components/page_loader';
|
||||
@import 'components/toggle_button_group';
|
||||
@import 'components/value-mappings';
|
||||
@import 'components/popover-box';
|
||||
|
||||
// LOAD @grafana/ui components
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.explore {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&-container {
|
||||
padding: $dashboard-padding;
|
||||
|
||||
Reference in New Issue
Block a user