mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into hugoh/bug-viewers-can-edit-not-working-for-explore
This commit is contained in:
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ authentication:
|
||||
|
||||
```bash
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
enabled = true
|
||||
allow_sign_up = false
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
|
||||
@@ -25,7 +25,6 @@ export class CustomScrollbar extends PureComponent<Props> {
|
||||
autoHideDuration: 200,
|
||||
autoMaxHeight: '100%',
|
||||
hideTracksWhenNotNeeded: false,
|
||||
scrollTop: 0,
|
||||
setScrollTop: () => {},
|
||||
autoHeightMin: '0'
|
||||
};
|
||||
|
||||
224
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
224
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { TimeSeriesVMs } from '../../types/series';
|
||||
import { ValueMapping, MappingType } from '../../types';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
timeSeries: {} as TimeSeriesVMs,
|
||||
decimals: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<Gauge {...props} />);
|
||||
const instance = wrapper.instance() as Gauge;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Get font color', () => {
|
||||
it('should get first threshold color when only one threshold', () => {
|
||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||
|
||||
expect(instance.getFontColor(49)).toEqual('#7EB26D');
|
||||
});
|
||||
|
||||
it('should get the threshold color if value is same as a threshold', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(instance.getFontColor(50)).toEqual('#EAB839');
|
||||
});
|
||||
|
||||
it('should get the nearest threshold color between thresholds', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(instance.getFontColor(55)).toEqual('#EAB839');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get thresholds formatted', () => {
|
||||
it('should return first thresholds color for min and max', () => {
|
||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||
|
||||
expect(instance.getFormattedThresholds()).toEqual([
|
||||
{ value: 0, color: '#7EB26D' },
|
||||
{ value: 100, color: '#7EB26D' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get the correct formatted values when thresholds are added', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(instance.getFormattedThresholds()).toEqual([
|
||||
{ value: 0, color: '#7EB26D' },
|
||||
{ value: 50, color: '#7EB26D' },
|
||||
{ value: 75, color: '#EAB839' },
|
||||
{ value: 100, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Format value with value mappings', () => {
|
||||
it('should return undefined with no valuemappings', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined with no matching valuemappings', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return first matching mapping with lowest id', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||
{ id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result.text).toEqual('1-20');
|
||||
});
|
||||
|
||||
it('should return rangeToText mapping where value equals to', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result.text).toEqual('1-10');
|
||||
});
|
||||
|
||||
it('should return rangeToText mapping where value equals from', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result.text).toEqual('10-20');
|
||||
});
|
||||
|
||||
it('should return rangeToText mapping where value is between from and to', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
|
||||
|
||||
expect(result.text).toEqual('1-20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Format value', () => {
|
||||
it('should return if value isNaN', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = 'N/A';
|
||||
const { instance } = setup({ valueMappings });
|
||||
|
||||
const result = instance.formatValue(value);
|
||||
|
||||
expect(result).toEqual('N/A');
|
||||
});
|
||||
|
||||
it('should return formatted value if there are no value mappings', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = '6';
|
||||
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||
|
||||
const result = instance.formatValue(value);
|
||||
|
||||
expect(result).toEqual(' 6.0 ');
|
||||
});
|
||||
|
||||
it('should return formatted value if there are no matching value mappings', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
|
||||
];
|
||||
const value = '10';
|
||||
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||
|
||||
const result = instance.formatValue(value);
|
||||
|
||||
expect(result).toEqual(' 10.0 ');
|
||||
});
|
||||
|
||||
it('should return mapped value if there are matching value mappings', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
];
|
||||
const value = '11';
|
||||
const { instance } = setup({ valueMappings, decimals: 1 });
|
||||
|
||||
const result = instance.formatValue(value);
|
||||
|
||||
expect(result).toEqual(' 1-20 ');
|
||||
});
|
||||
});
|
||||
284
packages/grafana-ui/src/components/Gauge/Gauge.tsx
Normal file
284
packages/grafana-ui/src/components/Gauge/Gauge.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
|
||||
import {
|
||||
ValueMapping,
|
||||
Threshold,
|
||||
ThemeName,
|
||||
MappingType,
|
||||
BasicGaugeColor,
|
||||
ThemeNames,
|
||||
ValueMap,
|
||||
RangeMap,
|
||||
} from '../../types/panel';
|
||||
import { TimeSeriesVMs } from '../../types/series';
|
||||
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||
|
||||
type TimeSeriesValue = string | number | null;
|
||||
|
||||
export interface Props {
|
||||
decimals: number;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
timeSeries: TimeSeriesVMs;
|
||||
thresholds: Threshold[];
|
||||
showThresholdMarkers: boolean;
|
||||
showThresholdLabels: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
width: number;
|
||||
theme?: ThemeName;
|
||||
}
|
||||
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
theme: ThemeNames.Dark,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) {
|
||||
if (!valueToTextMapping.value) {
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
const valueAsNumber = parseFloat(value as string);
|
||||
const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
|
||||
|
||||
if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
if (valueAsNumber !== valueToTextMappingAsNumber) {
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
return allValueMappings.concat(valueToTextMapping);
|
||||
}
|
||||
|
||||
addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) {
|
||||
if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) {
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
const valueAsNumber = parseFloat(value as string);
|
||||
const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
|
||||
const toAsNumber = parseFloat(rangeToTextMapping.to as string);
|
||||
|
||||
if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
|
||||
return allValueMappings.concat(rangeToTextMapping);
|
||||
}
|
||||
|
||||
return allValueMappings;
|
||||
}
|
||||
|
||||
getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) {
|
||||
const allFormattedValueMappings = valueMappings.reduce(
|
||||
(allValueMappings, valueMapping) => {
|
||||
if (valueMapping.type === MappingType.ValueToText) {
|
||||
allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
|
||||
} else if (valueMapping.type === MappingType.RangeToText) {
|
||||
allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
|
||||
}
|
||||
|
||||
return allValueMappings;
|
||||
},
|
||||
[] as ValueMapping[]
|
||||
);
|
||||
|
||||
allFormattedValueMappings.sort((t1, t2) => {
|
||||
return t1.id - t2.id;
|
||||
});
|
||||
|
||||
return allFormattedValueMappings;
|
||||
}
|
||||
|
||||
getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) {
|
||||
return this.getAllFormattedValueMappings(valueMappings, value)[0];
|
||||
}
|
||||
|
||||
formatValue(value: TimeSeriesValue) {
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
if (isNaN(value as number)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (valueMappings.length > 0) {
|
||||
const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value);
|
||||
if (valueMappedValue) {
|
||||
return `${prefix} ${valueMappedValue.text} ${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
const formatFunc = getValueFormat(unit);
|
||||
const formattedValue = formatFunc(value as number, decimals);
|
||||
|
||||
return `${prefix} ${formattedValue} ${suffix}`;
|
||||
}
|
||||
|
||||
getFontColor(value: TimeSeriesValue) {
|
||||
const { thresholds } = this.props;
|
||||
|
||||
if (thresholds.length === 1) {
|
||||
return thresholds[0].color;
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return atThreshold.color;
|
||||
}
|
||||
|
||||
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
||||
|
||||
if (belowThreshold.length > 0) {
|
||||
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
|
||||
return nearestThreshold.color;
|
||||
}
|
||||
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
|
||||
getFormattedThresholds() {
|
||||
const { maxValue, minValue, thresholds } = this.props;
|
||||
|
||||
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
|
||||
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
|
||||
|
||||
const formattedThresholds = [
|
||||
...thresholdsSortedByIndex.map(threshold => {
|
||||
if (threshold.index === 0) {
|
||||
return { value: minValue, color: threshold.color };
|
||||
}
|
||||
|
||||
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
|
||||
return { value: threshold.value, color: previousThreshold.color };
|
||||
}),
|
||||
{ value: maxValue, color: lastThreshold.color },
|
||||
];
|
||||
|
||||
return formattedThresholds;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const {
|
||||
maxValue,
|
||||
minValue,
|
||||
timeSeries,
|
||||
showThresholdLabels,
|
||||
showThresholdMarkers,
|
||||
width,
|
||||
height,
|
||||
stat,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let value: TimeSeriesValue = '';
|
||||
|
||||
if (timeSeries[0]) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
} else {
|
||||
value = 'N/A';
|
||||
}
|
||||
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const fontScale = parseInt('80', 10) / 100;
|
||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
width: gaugeWidth,
|
||||
},
|
||||
frame: { show: false },
|
||||
label: { show: false },
|
||||
layout: { margin: 0, thresholdWidth: 0 },
|
||||
cell: { border: { width: 0 } },
|
||||
threshold: {
|
||||
values: this.getFormattedThresholds(),
|
||||
label: {
|
||||
show: showThresholdLabels,
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
font: { size: thresholdLabelFontSize },
|
||||
},
|
||||
show: showThresholdMarkers,
|
||||
width: thresholdMarkersWidth,
|
||||
},
|
||||
value: {
|
||||
color: this.getFontColor(value),
|
||||
formatter: () => {
|
||||
return this.formatValue(value);
|
||||
},
|
||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = { data: [[0, value]] };
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
} catch (err) {
|
||||
console.log('Gauge rendering error', err, options, timeSeries);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className="singlestat-panel">
|
||||
<div
|
||||
style={{
|
||||
height: `${height * 0.9}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Gauge;
|
||||
@@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const thresholds: Threshold[] =
|
||||
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
|
||||
const addDefaultThreshold = this.props.thresholds.length === 0;
|
||||
const thresholds: Threshold[] = addDefaultThreshold
|
||||
? [{ index: 0, value: -Infinity, color: colors[0] }]
|
||||
: props.thresholds;
|
||||
this.state = { thresholds };
|
||||
|
||||
if (addDefaultThreshold) {
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
onAddThreshold = (index: number) => {
|
||||
@@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
},
|
||||
]),
|
||||
},
|
||||
() => this.updateGauge()
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
thresholds: newThresholds.filter(t => t !== threshold),
|
||||
};
|
||||
},
|
||||
() => this.updateGauge()
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,7 +105,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
const value = isNaN(parsedValue) ? null : parsedValue;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
if (t === threshold && t.index !== 0) {
|
||||
t = { ...t, value: value as number };
|
||||
}
|
||||
|
||||
@@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
},
|
||||
() => this.updateGauge()
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||
onBlur = () => {
|
||||
this.setState(prevState => {
|
||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||
@@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
return { thresholds: sortThresholds };
|
||||
});
|
||||
|
||||
this.updateGauge();
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
updateGauge = () => {
|
||||
onChange = () => {
|
||||
this.props.onChange(this.state.thresholds);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
|
||||
@@ -7,15 +7,33 @@ export interface DataQueryResponse {
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
/**
|
||||
* A - Z
|
||||
*/
|
||||
refId: string;
|
||||
[key: string]: any;
|
||||
|
||||
/**
|
||||
* true if query is disabled (ie not executed / sent to TSDB)
|
||||
*/
|
||||
hide?: boolean;
|
||||
|
||||
/**
|
||||
* Unique, guid like, string used in explore mode
|
||||
*/
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* For mixed data sources the selected datasource is on the query level.
|
||||
* For non mixed scenarios this is undefined.
|
||||
*/
|
||||
datasource?: string | null;
|
||||
}
|
||||
|
||||
export interface DataQueryOptions {
|
||||
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
|
||||
timezone: string;
|
||||
range: TimeRange;
|
||||
rangeRaw: RawTimeRange;
|
||||
targets: DataQuery[];
|
||||
targets: TQuery[];
|
||||
panelId: number;
|
||||
dashboardId: number;
|
||||
cacheTimeout?: string;
|
||||
|
||||
@@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export type ThemeName = 'dark' | 'light';
|
||||
|
||||
export enum ThemeNames {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@ import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
||||
|
||||
export interface DataSourceApi {
|
||||
name: string;
|
||||
meta: PluginMeta;
|
||||
pluginExports: PluginExports;
|
||||
|
||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
/**
|
||||
* min interval range
|
||||
*/
|
||||
@@ -15,7 +11,7 @@ export interface DataSourceApi {
|
||||
/**
|
||||
* Imports queries from a different datasource
|
||||
*/
|
||||
importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]>;
|
||||
importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
|
||||
|
||||
/**
|
||||
* Initializes a datasource after instantiation
|
||||
@@ -25,7 +21,7 @@ export interface DataSourceApi {
|
||||
/**
|
||||
* Main metrics / data query action
|
||||
*/
|
||||
query(options: DataQueryOptions): Promise<DataQueryResponse>;
|
||||
query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
|
||||
|
||||
/**
|
||||
* Test & verify datasource settings & connection details
|
||||
@@ -35,20 +31,27 @@ export interface DataSourceApi {
|
||||
/**
|
||||
* Get hints for query improvements
|
||||
*/
|
||||
getQueryHints(query: DataQuery, results: any[], ...rest: any): QueryHint[];
|
||||
getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
|
||||
|
||||
/**
|
||||
* Set after constructor is called by Grafana
|
||||
*/
|
||||
name?: string;
|
||||
meta?: PluginMeta;
|
||||
pluginExports?: PluginExports;
|
||||
}
|
||||
|
||||
export interface QueryEditorProps {
|
||||
datasource: DataSourceApi;
|
||||
query: DataQuery;
|
||||
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
onExecuteQuery?: () => void;
|
||||
onQueryChange?: (value: DataQuery) => void;
|
||||
onQueryChange?: (value: TQuery) => void;
|
||||
}
|
||||
|
||||
export interface PluginExports {
|
||||
Datasource?: any;
|
||||
Datasource?: DataSourceApi;
|
||||
QueryCtrl?: any;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps>;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
|
||||
ConfigCtrl?: any;
|
||||
AnnotationsQueryCtrl?: any;
|
||||
VariableQueryEditor?: any;
|
||||
|
||||
@@ -21,9 +21,12 @@ export interface TimeSeriesVM {
|
||||
color: string;
|
||||
data: TimeSeriesValue[][];
|
||||
stats: TimeSeriesStats;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
}
|
||||
|
||||
export interface TimeSeriesStats {
|
||||
[key: string]: number | null;
|
||||
total: number | null;
|
||||
max: number | null;
|
||||
min: number | null;
|
||||
@@ -36,8 +39,6 @@ export interface TimeSeriesStats {
|
||||
range: number | null;
|
||||
timeStep: number;
|
||||
count: number;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
}
|
||||
|
||||
export enum NullValueMode {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
import { colors } from './colors';
|
||||
|
||||
// Types
|
||||
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
||||
|
||||
interface Options {
|
||||
timeSeries: TimeSeries[];
|
||||
nullValueMode: NullValueMode;
|
||||
colorPalette: string[];
|
||||
}
|
||||
|
||||
export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs {
|
||||
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
|
||||
const vmSeries = timeSeries.map((item, index) => {
|
||||
const colorIndex = index % colorPalette.length;
|
||||
const colorIndex = index % colors.length;
|
||||
const label = item.target;
|
||||
const result = [];
|
||||
|
||||
@@ -49,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof currentValue !== 'number') {
|
||||
continue;
|
||||
if (currentValue !== null && typeof currentValue !== 'number') {
|
||||
throw {message: 'Time series contains non number values'};
|
||||
}
|
||||
|
||||
// Due to missing values we could have different timeStep all along the series
|
||||
@@ -150,7 +151,9 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
||||
return {
|
||||
data: result,
|
||||
label: label,
|
||||
color: colorPalette[colorIndex],
|
||||
color: colors[colorIndex],
|
||||
allIsZero,
|
||||
allIsNull,
|
||||
stats: {
|
||||
total,
|
||||
min,
|
||||
@@ -164,8 +167,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
||||
range,
|
||||
count,
|
||||
first,
|
||||
allIsZero,
|
||||
allIsNull,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import store from 'app/core/store';
|
||||
import { ThemeNames, ThemeName } from '@grafana/ui';
|
||||
|
||||
export class User {
|
||||
isGrafanaAdmin: any;
|
||||
@@ -63,6 +64,10 @@ export class ContextSrv {
|
||||
hasAccessToExplore() {
|
||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||
}
|
||||
|
||||
getTheme(): ThemeName {
|
||||
return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
|
||||
}
|
||||
}
|
||||
|
||||
const contextSrv = new ContextSrv();
|
||||
|
||||
@@ -203,7 +203,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
||||
/**
|
||||
* A target is non-empty when it has keys (with non-empty values) other than refId and key.
|
||||
*/
|
||||
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
|
||||
export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
|
||||
return (
|
||||
queries &&
|
||||
queries.some(
|
||||
@@ -280,7 +280,11 @@ export function makeTimeSeriesList(dataList) {
|
||||
/**
|
||||
* Update the query history. Side-effect: store history in local storage
|
||||
*/
|
||||
export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
|
||||
export function updateHistory<T extends DataQuery = any>(
|
||||
history: Array<HistoryItem<T>>,
|
||||
datasourceId: string,
|
||||
queries: T[]
|
||||
): Array<HistoryItem<T>> {
|
||||
const ts = Date.now();
|
||||
queries.forEach(query => {
|
||||
history = [{ query, ts }, ...history];
|
||||
|
||||
@@ -15,7 +15,7 @@ interface State {
|
||||
}
|
||||
|
||||
export class PanelResizer extends PureComponent<Props, State> {
|
||||
initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.4);
|
||||
initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.3);
|
||||
prevEditorHeight: number;
|
||||
throttledChangeHeight: (height: number) => void;
|
||||
throttledResizeDone: () => void;
|
||||
|
||||
@@ -111,14 +111,11 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar">
|
||||
<div className="toolbar__heading">{heading}</div>
|
||||
{renderToolbar && renderToolbar()}
|
||||
{toolbarItems.length > 0 && (
|
||||
<>
|
||||
<div className="gf-form--grow" />
|
||||
{toolbarItems.map(item => this.renderButton(item))}
|
||||
</>
|
||||
)}
|
||||
<div className="toolbar__left">
|
||||
<div className="toolbar__heading">{heading}</div>
|
||||
{renderToolbar && renderToolbar()}
|
||||
</div>
|
||||
{toolbarItems.map(item => this.renderButton(item))}
|
||||
</div>
|
||||
<div className="panel-editor__scroll">
|
||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import config from 'app/core/config';
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
|
||||
import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
|
||||
interface Props {
|
||||
@@ -133,14 +133,13 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||
<div className="m-l-2">
|
||||
{!isAddingMixed && (
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||
Add Query
|
||||
</button>
|
||||
)}
|
||||
{isAddingMixed && this.renderMixedPicker()}
|
||||
</div>
|
||||
<div className="flex-grow" />
|
||||
{!isAddingMixed && (
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||
Add Query
|
||||
</button>
|
||||
)}
|
||||
{isAddingMixed && this.renderMixedPicker()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
target: query,
|
||||
panel: panel,
|
||||
refresh: () => panel.refresh(),
|
||||
render: () => panel.render,
|
||||
render: () => panel.render(),
|
||||
events: panel.events,
|
||||
};
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
{inMixedMode && <em className="query-editor-row__context-info"> ({datasourceName})</em>}
|
||||
{isDisabled && <em className="query-editor-row__context-info"> Disabled</em>}
|
||||
</div>
|
||||
<div className="query-editor-row__collapsed-text">
|
||||
<div className="query-editor-row__collapsed-text" onClick={this.onToggleEditMode}>
|
||||
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
||||
</div>
|
||||
<div className="query-editor-row__actions">
|
||||
|
||||
@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { response, isLoading } = this.state.dsQuery;
|
||||
const { isMocking } = this.state;
|
||||
const openNodes = this.getNrOfOpenNodes();
|
||||
|
||||
if (isLoading) {
|
||||
@@ -199,20 +198,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
|
||||
{!isMocking && <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />}
|
||||
{isMocking && (
|
||||
<div className="query-troubleshooter__body">
|
||||
<div className="gf-form p-l-1 gf-form--v-stretch">
|
||||
<textarea
|
||||
className="gf-form-input"
|
||||
style={{ width: '95%' }}
|
||||
rows={10}
|
||||
onInput={this.setMockedResponse}
|
||||
placeholder="JSON"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ const mustKeepProps: { [str: string]: boolean } = {
|
||||
hasRefreshed: true,
|
||||
events: true,
|
||||
cacheTimeout: true,
|
||||
nullPointMode: true,
|
||||
cachedPluginOptions: true,
|
||||
transparent: true,
|
||||
};
|
||||
@@ -244,8 +243,6 @@ export class PanelModel {
|
||||
addQuery(query?: Partial<DataQuery>) {
|
||||
query = query || { refId: 'A' };
|
||||
query.refId = this.getNextQueryLetter();
|
||||
query.isNew = true;
|
||||
|
||||
this.targets.push(query);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,11 +242,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="navbar-buttons explore-first-button">
|
||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||
Close Split
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<div className="navbar-page-btn" />
|
||||
<div className="navbar-buttons explore-first-button">
|
||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||
Close Split
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!datasourceMissing ? (
|
||||
<div className="navbar-buttons">
|
||||
@@ -274,7 +277,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
<div className="navbar-buttons relative">
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
|
||||
Run Query{' '}
|
||||
{loading ? <i className="fa fa-spinner fa-fw fa-spin run-icon" /> : <i className="fa fa-level-down fa-fw run-icon" />}
|
||||
{loading ? (
|
||||
<i className="fa fa-spinner fa-fw fa-spin run-icon" />
|
||||
) : (
|
||||
<i className="fa fa-level-down fa-fw run-icon" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
// Services
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { getIntervals } from 'app/core/utils/explore';
|
||||
import { getTimeSrv } from 'app/features/dashboard/time_srv';
|
||||
|
||||
// Types
|
||||
@@ -37,8 +36,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||
const template = '<plugin-component type="query-ctrl"> </plugin-component>';
|
||||
const target = { datasource: datasource.name, ...initialQuery };
|
||||
const scopeProps = {
|
||||
target,
|
||||
ctrl: {
|
||||
datasource,
|
||||
target,
|
||||
refresh: () => {
|
||||
this.props.onQueryChange(target, false);
|
||||
this.props.onExecuteQuery();
|
||||
@@ -48,11 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||
datasource,
|
||||
targets: [target],
|
||||
},
|
||||
dashboard: {
|
||||
getNextQueryLetter: x => '',
|
||||
},
|
||||
hideEditorRowActions: true,
|
||||
...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
|
||||
dashboard: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
// Components
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
|
||||
// Types
|
||||
import { LokiQuery } from '../types';
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
|
||||
@@ -63,10 +68,10 @@ interface LokiQueryFieldProps {
|
||||
error?: string | JSX.Element;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
initialQuery?: DataQuery;
|
||||
initialQuery?: LokiQuery;
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: DataQuery, override?: boolean) => void;
|
||||
onQueryChange?: (value: LokiQuery, override?: boolean) => void;
|
||||
}
|
||||
|
||||
interface LokiQueryFieldState {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import LokiDatasource from './datasource';
|
||||
import { LokiQuery } from './types';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
|
||||
describe('LokiDatasource', () => {
|
||||
const instanceSettings: any = {
|
||||
@@ -13,12 +15,13 @@ describe('LokiDatasource', () => {
|
||||
replace: a => a,
|
||||
};
|
||||
|
||||
const range = { from: 'now-6h', to: 'now' };
|
||||
|
||||
test('should use default max lines when no limit given', () => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
ds.query({ range, targets: [{ expr: 'foo' }] });
|
||||
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||
|
||||
ds.query(options);
|
||||
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
|
||||
});
|
||||
@@ -28,7 +31,10 @@ describe('LokiDatasource', () => {
|
||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
ds.query({ range, targets: [{ expr: 'foo' }] });
|
||||
|
||||
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||
ds.query(options);
|
||||
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
||||
});
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & Utils
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
||||
import { PluginMeta, DataQuery } from '@grafana/ui/src/types';
|
||||
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||
|
||||
import LanguageProvider from './language_provider';
|
||||
import { mergeStreamsToLogs } from './result_transformer';
|
||||
import { formatQuery, parseQuery } from './query_utils';
|
||||
import { makeSeriesForLogs } from 'app/core/logs_model';
|
||||
|
||||
// Types
|
||||
import { LogsStream, LogsModel } from 'app/core/logs_model';
|
||||
import { PluginMeta, DataQueryOptions } from '@grafana/ui/src/types';
|
||||
import { LokiQuery } from './types';
|
||||
|
||||
export const DEFAULT_MAX_LINES = 1000;
|
||||
|
||||
@@ -68,7 +73,7 @@ export default class LokiDatasource {
|
||||
};
|
||||
}
|
||||
|
||||
query(options): Promise<{ data: LogsStream[] }> {
|
||||
query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
|
||||
const queryTargets = options.targets
|
||||
.filter(target => target.expr)
|
||||
.map(target => this.prepareQueryTarget(target, options));
|
||||
@@ -96,7 +101,7 @@ export default class LokiDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]> {
|
||||
async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
|
||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
||||
}
|
||||
|
||||
@@ -109,7 +114,7 @@ export default class LokiDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
modifyQuery(query: DataQuery, action: any): DataQuery {
|
||||
modifyQuery(query: LokiQuery, action: any): LokiQuery {
|
||||
const parsed = parseQuery(query.expr || '');
|
||||
let selector = parsed.query;
|
||||
switch (action.type) {
|
||||
@@ -124,7 +129,7 @@ export default class LokiDatasource {
|
||||
return { ...query, expr: expression };
|
||||
}
|
||||
|
||||
getHighlighterExpression(query: DataQuery): string {
|
||||
getHighlighterExpression(query: LokiQuery): string {
|
||||
return parseQuery(query.expr).regexp;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// Services & Utils
|
||||
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
||||
import syntax from './syntax';
|
||||
|
||||
// Types
|
||||
import {
|
||||
CompletionItem,
|
||||
CompletionItemGroup,
|
||||
@@ -9,9 +15,7 @@ import {
|
||||
TypeaheadOutput,
|
||||
HistoryItem,
|
||||
} from 'app/types/explore';
|
||||
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
||||
import syntax from './syntax';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { LokiQuery } from './types';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
@@ -20,7 +24,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
|
||||
const wrapLabel = (label: string) => ({ label });
|
||||
|
||||
export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem {
|
||||
type LokiHistoryItem = HistoryItem<LokiQuery>;
|
||||
|
||||
export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem {
|
||||
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||
const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label);
|
||||
const count = historyForItem.length;
|
||||
@@ -155,7 +161,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
return { context, refresher, suggestions };
|
||||
}
|
||||
|
||||
async importQueries(queries: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
|
||||
async importQueries(queries: LokiQuery[], datasourceType: string): Promise<LokiQuery[]> {
|
||||
if (datasourceType === 'prometheus') {
|
||||
return Promise.all(
|
||||
queries.map(async query => {
|
||||
|
||||
6
public/app/plugins/datasource/loki/types.ts
Normal file
6
public/app/plugins/datasource/loki/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export interface LokiQuery extends DataQuery {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
@@ -88,13 +88,13 @@ interface CascaderOption {
|
||||
interface PromQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
initialQuery: DataQuery;
|
||||
initialQuery: PromQuery;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
metricsByPrefix?: CascaderOption[];
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: DataQuery, override?: boolean) => void;
|
||||
onQueryChange?: (value: PromQuery, override?: boolean) => void;
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
@@ -166,7 +166,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
// Send text change to parent
|
||||
const { initialQuery, onQueryChange } = this.props;
|
||||
if (onQueryChange) {
|
||||
const query: DataQuery = {
|
||||
const query: PromQuery = {
|
||||
...initialQuery,
|
||||
expr: value,
|
||||
};
|
||||
|
||||
@@ -1,57 +1,24 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
// Services & Utils
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import { ResultTransformer } from './result_transformer';
|
||||
import PrometheusLanguageProvider from './language_provider';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import addLabelToQuery from './add_label_to_query';
|
||||
import { getQueryHints } from './query_hints';
|
||||
import { expandRecordingRules } from './language_utils';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
// Types
|
||||
import { PromQuery } from './types';
|
||||
import { DataQueryOptions, DataSourceApi } from '@grafana/ui/src/types';
|
||||
import { ExploreUrlState } from 'app/types/explore';
|
||||
|
||||
export function alignRange(start, end, step) {
|
||||
const alignedEnd = Math.ceil(end / step) * step;
|
||||
const alignedStart = Math.floor(start / step) * step;
|
||||
return {
|
||||
end: alignedEnd,
|
||||
start: alignedStart,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractRuleMappingFromGroups(groups: any[]) {
|
||||
return groups.reduce(
|
||||
(mapping, group) =>
|
||||
group.rules.filter(rule => rule.type === 'recording').reduce(
|
||||
(acc, rule) => ({
|
||||
...acc,
|
||||
[rule.name]: rule.query,
|
||||
}),
|
||||
mapping
|
||||
),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function prometheusRegularEscape(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/'/g, "\\\\'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function prometheusSpecialRegexEscape(value) {
|
||||
if (typeof value === 'string') {
|
||||
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export class PrometheusDatasource {
|
||||
export class PrometheusDatasource implements DataSourceApi<PromQuery> {
|
||||
type: string;
|
||||
editorSrc: string;
|
||||
name: string;
|
||||
@@ -149,7 +116,7 @@ export class PrometheusDatasource {
|
||||
return this.templateSrv.variableExists(target.expr);
|
||||
}
|
||||
|
||||
query(options) {
|
||||
query(options: DataQueryOptions<PromQuery>) {
|
||||
const start = this.getPrometheusTime(options.range.from, false);
|
||||
const end = this.getPrometheusTime(options.range.to, true);
|
||||
|
||||
@@ -423,7 +390,7 @@ export class PrometheusDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
getExploreState(queries: DataQuery[]): Partial<ExploreUrlState> {
|
||||
getExploreState(queries: PromQuery[]): Partial<ExploreUrlState> {
|
||||
let state: Partial<ExploreUrlState> = { datasource: this.name };
|
||||
if (queries && queries.length > 0) {
|
||||
const expandedQueries = queries.map(query => ({
|
||||
@@ -438,7 +405,7 @@ export class PrometheusDatasource {
|
||||
return state;
|
||||
}
|
||||
|
||||
getQueryHints(query: DataQuery, result: any[]) {
|
||||
getQueryHints(query: PromQuery, result: any[]) {
|
||||
return getQueryHints(query.expr || '', result, this);
|
||||
}
|
||||
|
||||
@@ -457,7 +424,7 @@ export class PrometheusDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
modifyQuery(query: DataQuery, action: any): DataQuery {
|
||||
modifyQuery(query: PromQuery, action: any): PromQuery {
|
||||
let expression = query.expr || '';
|
||||
switch (action.type) {
|
||||
case 'ADD_FILTER': {
|
||||
@@ -507,3 +474,40 @@ export class PrometheusDatasource {
|
||||
return this.resultTransformer.getOriginalMetricName(labelData);
|
||||
}
|
||||
}
|
||||
|
||||
export function alignRange(start, end, step) {
|
||||
const alignedEnd = Math.ceil(end / step) * step;
|
||||
const alignedStart = Math.floor(start / step) * step;
|
||||
return {
|
||||
end: alignedEnd,
|
||||
start: alignedStart,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractRuleMappingFromGroups(groups: any[]) {
|
||||
return groups.reduce(
|
||||
(mapping, group) =>
|
||||
group.rules.filter(rule => rule.type === 'recording').reduce(
|
||||
(acc, rule) => ({
|
||||
...acc,
|
||||
[rule.name]: rule.query,
|
||||
}),
|
||||
mapping
|
||||
),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function prometheusRegularEscape(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/'/g, "\\\\'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function prometheusSpecialRegexEscape(value) {
|
||||
if (typeof value === 'string') {
|
||||
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
6
public/app/plugins/datasource/prometheus/types.ts
Normal file
6
public/app/plugins/datasource/prometheus/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export interface PromQuery extends DataQuery {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,17 @@ import { FormLabel, Select, SelectOptionItem } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { QueryEditorProps } from '@grafana/ui/src/types';
|
||||
|
||||
interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
import { TestDataDatasource } from './datasource';
|
||||
import { TestDataQuery, Scenario } from './types';
|
||||
|
||||
interface State {
|
||||
scenarioList: Scenario[];
|
||||
current: Scenario | null;
|
||||
}
|
||||
|
||||
export class QueryEditor extends PureComponent<QueryEditorProps> {
|
||||
type Props = QueryEditorProps<TestDataDatasource, TestDataQuery>;
|
||||
|
||||
export class QueryEditor extends PureComponent<Props> {
|
||||
backendSrv: BackendSrv = getBackendSrv();
|
||||
|
||||
state: State = {
|
||||
@@ -30,11 +29,12 @@ export class QueryEditor extends PureComponent<QueryEditorProps> {
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { query } = this.props;
|
||||
const { query, datasource } = this.props;
|
||||
|
||||
query.scenarioId = query.scenarioId || 'random_walk';
|
||||
|
||||
const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios');
|
||||
// const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios');
|
||||
const scenarioList = await datasource.getScenarios();
|
||||
const current = _.find(scenarioList, { id: query.scenarioId });
|
||||
|
||||
this.setState({ scenarioList: scenarioList, current: current });
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { DataSourceApi, DataQueryOptions } from '@grafana/ui';
|
||||
import { TestDataQuery, Scenario } from './types';
|
||||
|
||||
class TestDataDatasource {
|
||||
id: any;
|
||||
export class TestDataDatasource implements DataSourceApi<TestDataQuery> {
|
||||
id: number;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private backendSrv, private $q) {
|
||||
this.id = instanceSettings.id;
|
||||
}
|
||||
|
||||
query(options) {
|
||||
query(options: DataQueryOptions<TestDataQuery>) {
|
||||
const queries = _.filter(options.targets, item => {
|
||||
return item.hide !== true;
|
||||
}).map(item => {
|
||||
@@ -91,6 +93,9 @@ class TestDataDatasource {
|
||||
message: 'Data source is working',
|
||||
});
|
||||
}
|
||||
|
||||
getScenarios(): Promise<Scenario[]> {
|
||||
return this.backendSrv.get('/api/tsdb/testdata/scenarios');
|
||||
}
|
||||
}
|
||||
|
||||
export { TestDataDatasource };
|
||||
|
||||
11
public/app/plugins/datasource/testdata/types.ts
vendored
Normal file
11
public/app/plugins/datasource/testdata/types.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export interface TestDataQuery extends DataQuery {
|
||||
scenarioId: string;
|
||||
}
|
||||
|
||||
export interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelProps, NullValueMode } from '@grafana/ui';
|
||||
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
// Services & Utils
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { processTimeSeries } from '@grafana/ui';
|
||||
|
||||
// Components
|
||||
import { Gauge } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
|
||||
|
||||
interface Props extends PanelProps<GaugeOptions> {}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
||||
|
||||
const prefix = onInterpolate(options.prefix);
|
||||
const suffix = onInterpolate(options.suffix);
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
const vmSeries = processTimeSeries({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -27,6 +35,7 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
theme={contextSrv.getTheme()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
BasicGaugeColor,
|
||||
PanelOptionsProps,
|
||||
ThresholdsEditor,
|
||||
Threshold,
|
||||
@@ -15,7 +14,6 @@ import { GaugeOptions } from './types';
|
||||
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||
|
||||
export interface GaugeOptions {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
// Utils
|
||||
import { processTimeSeries } from '@grafana/ui/src/utils';
|
||||
@@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent<Props> {
|
||||
const vmSeries = processTimeSeries({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
colorPalette: colors,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -243,9 +243,9 @@ export interface ExploreUrlState {
|
||||
range: RawTimeRange;
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||
ts: number;
|
||||
query: DataQuery;
|
||||
query: TQuery;
|
||||
}
|
||||
|
||||
export abstract class LanguageProvider {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
timeSeries: {} as TimeSeriesVMs,
|
||||
decimals: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<Gauge {...props} />);
|
||||
const instance = wrapper.instance() as Gauge;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Get font color', () => {
|
||||
it('should get base color if no threshold', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
|
||||
});
|
||||
|
||||
it('should be f2f2f2', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [{ value: 59, color: '#f2f2f2' }],
|
||||
});
|
||||
|
||||
expect(instance.getFontColor(58)).toEqual('#f2f2f2');
|
||||
});
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
|
||||
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
|
||||
export interface Props {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
timeSeries: TimeSeriesVMs;
|
||||
thresholds: Threshold[];
|
||||
showThresholdMarkers: boolean;
|
||||
showThresholdLabels: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatWithMappings(mappings, value) {
|
||||
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
||||
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
||||
|
||||
const valueMap = valueMaps.map(mapping => {
|
||||
if (mapping.value && value === mapping.value) {
|
||||
return mapping.text;
|
||||
}
|
||||
})[0];
|
||||
|
||||
const rangeMap = rangeMaps.map(mapping => {
|
||||
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
||||
return mapping.text;
|
||||
}
|
||||
})[0];
|
||||
|
||||
return { rangeMap, valueMap };
|
||||
}
|
||||
|
||||
formatValue(value) {
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
const formatFunc = kbn.valueFormats[unit];
|
||||
const formattedValue = formatFunc(value, decimals);
|
||||
|
||||
if (valueMappings.length > 0) {
|
||||
const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
|
||||
|
||||
if (valueMap) {
|
||||
return `${prefix} ${valueMap} ${suffix}`;
|
||||
} else if (rangeMap) {
|
||||
return `${prefix} ${rangeMap} ${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return `${prefix} ${formattedValue} ${suffix}`;
|
||||
}
|
||||
|
||||
getFontColor(value) {
|
||||
const { baseColor, maxValue, thresholds } = this.props;
|
||||
|
||||
if (thresholds.length > 0) {
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const {
|
||||
baseColor,
|
||||
maxValue,
|
||||
minValue,
|
||||
timeSeries,
|
||||
showThresholdLabels,
|
||||
showThresholdMarkers,
|
||||
thresholds,
|
||||
width,
|
||||
height,
|
||||
stat,
|
||||
} = this.props;
|
||||
|
||||
let value: string | number = '';
|
||||
|
||||
if (timeSeries[0]) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
} else {
|
||||
value = 'N/A';
|
||||
}
|
||||
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const fontScale = parseInt('80', 10) / 100;
|
||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const formattedThresholds = [
|
||||
{ value: minValue, color: BasicGaugeColor.Green },
|
||||
...thresholds.map((threshold, index) => {
|
||||
return {
|
||||
value: threshold.value,
|
||||
color: index === 0 ? threshold.color : thresholds[index].color,
|
||||
};
|
||||
}),
|
||||
{ value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
|
||||
];
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
width: gaugeWidth,
|
||||
},
|
||||
frame: { show: false },
|
||||
label: { show: false },
|
||||
layout: { margin: 0, thresholdWidth: 0 },
|
||||
cell: { border: { width: 0 } },
|
||||
threshold: {
|
||||
values: formattedThresholds,
|
||||
label: {
|
||||
show: showThresholdLabels,
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
font: { size: thresholdLabelFontSize },
|
||||
},
|
||||
show: showThresholdMarkers,
|
||||
width: thresholdMarkersWidth,
|
||||
},
|
||||
value: {
|
||||
color: this.getFontColor(value),
|
||||
formatter: () => {
|
||||
return this.formatValue(value);
|
||||
},
|
||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = { data: [[0, value]] };
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
} catch (err) {
|
||||
console.log('Gauge rendering error', err, options, timeSeries);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className="singlestat-panel">
|
||||
<div
|
||||
style={{
|
||||
height: `${height * 0.9}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Gauge;
|
||||
@@ -1,168 +0,0 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
|
||||
|
||||
interface Options {
|
||||
timeSeries: TimeSeries[];
|
||||
nullValueMode: NullValueMode;
|
||||
}
|
||||
|
||||
export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
|
||||
const vmSeries = timeSeries.map((item, index) => {
|
||||
const colorIndex = index % colors.length;
|
||||
const label = item.target;
|
||||
const result = [];
|
||||
|
||||
// stat defaults
|
||||
let total = 0;
|
||||
let max = -Number.MAX_VALUE;
|
||||
let min = Number.MAX_VALUE;
|
||||
let logmin = Number.MAX_VALUE;
|
||||
let avg = null;
|
||||
let current = null;
|
||||
let first = null;
|
||||
let delta = 0;
|
||||
let diff = null;
|
||||
let range = null;
|
||||
let timeStep = Number.MAX_VALUE;
|
||||
let allIsNull = true;
|
||||
let allIsZero = true;
|
||||
|
||||
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
||||
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
||||
|
||||
let currentTime;
|
||||
let currentValue;
|
||||
let nonNulls = 0;
|
||||
let previousTime;
|
||||
let previousValue = 0;
|
||||
let previousDeltaUp = true;
|
||||
|
||||
for (let i = 0; i < item.datapoints.length; i++) {
|
||||
currentValue = item.datapoints[i][0];
|
||||
currentTime = item.datapoints[i][1];
|
||||
|
||||
// Due to missing values we could have different timeStep all along the series
|
||||
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
|
||||
if (previousTime !== undefined) {
|
||||
const currentStep = currentTime - previousTime;
|
||||
if (currentStep < timeStep) {
|
||||
timeStep = currentStep;
|
||||
}
|
||||
}
|
||||
|
||||
previousTime = currentTime;
|
||||
|
||||
if (currentValue === null) {
|
||||
if (ignoreNulls) {
|
||||
continue;
|
||||
}
|
||||
if (nullAsZero) {
|
||||
currentValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentValue !== null) {
|
||||
if (_.isNumber(currentValue)) {
|
||||
total += currentValue;
|
||||
allIsNull = false;
|
||||
nonNulls++;
|
||||
}
|
||||
|
||||
if (currentValue > max) {
|
||||
max = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue < min) {
|
||||
min = currentValue;
|
||||
}
|
||||
|
||||
if (first === null) {
|
||||
first = currentValue;
|
||||
} else {
|
||||
if (previousValue > currentValue) {
|
||||
// counter reset
|
||||
previousDeltaUp = false;
|
||||
if (i === item.datapoints.length - 1) {
|
||||
// reset on last
|
||||
delta += currentValue;
|
||||
}
|
||||
} else {
|
||||
if (previousDeltaUp) {
|
||||
delta += currentValue - previousValue; // normal increment
|
||||
} else {
|
||||
delta += currentValue; // account for counter reset
|
||||
}
|
||||
previousDeltaUp = true;
|
||||
}
|
||||
}
|
||||
previousValue = currentValue;
|
||||
|
||||
if (currentValue < logmin && currentValue > 0) {
|
||||
logmin = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue !== 0) {
|
||||
allIsZero = false;
|
||||
}
|
||||
}
|
||||
|
||||
result.push([currentTime, currentValue]);
|
||||
}
|
||||
|
||||
if (max === -Number.MAX_VALUE) {
|
||||
max = null;
|
||||
}
|
||||
|
||||
if (min === Number.MAX_VALUE) {
|
||||
min = null;
|
||||
}
|
||||
|
||||
if (result.length && !allIsNull) {
|
||||
avg = total / nonNulls;
|
||||
current = result[result.length - 1][1];
|
||||
if (current === null && result.length > 1) {
|
||||
current = result[result.length - 2][1];
|
||||
}
|
||||
}
|
||||
|
||||
if (max !== null && min !== null) {
|
||||
range = max - min;
|
||||
}
|
||||
|
||||
if (current !== null && first !== null) {
|
||||
diff = current - first;
|
||||
}
|
||||
|
||||
const count = result.length;
|
||||
|
||||
return {
|
||||
data: result,
|
||||
label: label,
|
||||
color: colors[colorIndex],
|
||||
stats: {
|
||||
total,
|
||||
min,
|
||||
max,
|
||||
current,
|
||||
logmin,
|
||||
avg,
|
||||
diff,
|
||||
delta,
|
||||
timeStep,
|
||||
range,
|
||||
count,
|
||||
first,
|
||||
allIsZero,
|
||||
allIsNull,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return vmSeries;
|
||||
}
|
||||
@@ -31,48 +31,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-query-content {
|
||||
flex-grow: 2;
|
||||
|
||||
&--collapsed {
|
||||
overflow: hidden;
|
||||
|
||||
.gf-form-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-query-letter-cell {
|
||||
flex-shrink: 0;
|
||||
|
||||
.gf-form-query-letter-cell-carret {
|
||||
display: inline-block;
|
||||
width: 0.7rem;
|
||||
position: relative;
|
||||
left: -2px;
|
||||
}
|
||||
.gf-form-query-letter-cell-letter {
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
.gf-form-query-letter-cell-ds {
|
||||
color: $text-color-weak;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-query-ds-label {
|
||||
text-align: center;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.grafana-metric-options {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.tight-form-func {
|
||||
background: $tight-form-func-bg;
|
||||
|
||||
@@ -124,28 +82,6 @@ input[type='text'].tight-form-func-param {
|
||||
}
|
||||
}
|
||||
|
||||
.query-troubleshooter {
|
||||
font-size: $font-size-sm;
|
||||
margin: $gf-form-margin;
|
||||
border: 1px solid $btn-secondary-bg;
|
||||
min-height: 100px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.query-troubleshooter__header {
|
||||
float: right;
|
||||
font-size: $font-size-sm;
|
||||
text-align: right;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
a {
|
||||
margin-left: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.query-troubleshooter__body {
|
||||
padding: $spacer 0;
|
||||
}
|
||||
|
||||
.rst-text::before {
|
||||
content: ' ';
|
||||
}
|
||||
@@ -202,8 +138,8 @@ input[type='text'].tight-form-func-param {
|
||||
background: $page-bg;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
.query-editor-row__ref-id {
|
||||
font-weight: $font-weight-semi-bold;
|
||||
color: $blue;
|
||||
@@ -256,7 +192,7 @@ input[type='text'].tight-form-func-param {
|
||||
}
|
||||
|
||||
.query-editor-row__body {
|
||||
margin: 0 0 10px 40px;
|
||||
margin: 2px 0 10px 40px;
|
||||
background: $page-bg;
|
||||
|
||||
&--collapsed {
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.toolbar__left {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar__main {
|
||||
padding: 0 $input-padding-x;
|
||||
font-size: $font-size-md;
|
||||
|
||||
25
public/test/helpers/getQueryOptions.ts
Normal file
25
public/test/helpers/getQueryOptions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DataQueryOptions, DataQuery } from '@grafana/ui';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
export function getQueryOptions<TQuery extends DataQuery>(options: Partial<DataQueryOptions<TQuery>>): DataQueryOptions<TQuery> {
|
||||
const raw = {from: 'now', to: 'now-1h'};
|
||||
const range = { from: moment(), to: moment(), raw: raw};
|
||||
|
||||
const defaults: DataQueryOptions<TQuery> = {
|
||||
range: range,
|
||||
rangeRaw: raw,
|
||||
targets: [],
|
||||
scopedVars: {},
|
||||
timezone: 'browser',
|
||||
panelId: 1,
|
||||
dashboardId: 1,
|
||||
interval: '60s',
|
||||
intervalMs: 60000,
|
||||
maxDataPoints: 500,
|
||||
};
|
||||
|
||||
Object.assign(defaults, options);
|
||||
|
||||
return defaults;
|
||||
}
|
||||
Reference in New Issue
Block a user