Merge branch 'master' into hugoh/bug-viewers-can-edit-not-working-for-explore
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ authentication:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
[auth.gitlab]
|
[auth.gitlab]
|
||||||
enabled = false
|
enabled = true
|
||||||
allow_sign_up = false
|
allow_sign_up = false
|
||||||
client_id = GITLAB_APPLICATION_ID
|
client_id = GITLAB_APPLICATION_ID
|
||||||
client_secret = GITLAB_SECRET
|
client_secret = GITLAB_SECRET
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class CustomScrollbar extends PureComponent<Props> {
|
|||||||
autoHideDuration: 200,
|
autoHideDuration: 200,
|
||||||
autoMaxHeight: '100%',
|
autoMaxHeight: '100%',
|
||||||
hideTracksWhenNotNeeded: false,
|
hideTracksWhenNotNeeded: false,
|
||||||
scrollTop: 0,
|
|
||||||
setScrollTop: () => {},
|
setScrollTop: () => {},
|
||||||
autoHeightMin: '0'
|
autoHeightMin: '0'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 ');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const thresholds: Threshold[] =
|
const addDefaultThreshold = this.props.thresholds.length === 0;
|
||||||
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
|
const thresholds: Threshold[] = addDefaultThreshold
|
||||||
|
? [{ index: 0, value: -Infinity, color: colors[0] }]
|
||||||
|
: props.thresholds;
|
||||||
this.state = { thresholds };
|
this.state = { thresholds };
|
||||||
|
|
||||||
|
if (addDefaultThreshold) {
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddThreshold = (index: number) => {
|
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),
|
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 value = isNaN(parsedValue) ? null : parsedValue;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(t => {
|
const newThresholds = thresholds.map(t => {
|
||||||
if (t === threshold) {
|
if (t === threshold && t.index !== 0) {
|
||||||
t = { ...t, value: value as number };
|
t = { ...t, value: value as number };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
{
|
{
|
||||||
thresholds: newThresholds,
|
thresholds: newThresholds,
|
||||||
},
|
},
|
||||||
() => this.updateGauge()
|
() => this.onChange()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||||
@@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
return { thresholds: sortThresholds };
|
return { thresholds: sortThresholds };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateGauge();
|
this.onChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateGauge = () => {
|
onChange = () => {
|
||||||
this.props.onChange(this.state.thresholds);
|
this.props.onChange(this.state.thresholds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph';
|
|||||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||||
|
export { Gauge } from './Gauge/Gauge';
|
||||||
|
|||||||
@@ -7,15 +7,33 @@ export interface DataQueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DataQuery {
|
export interface DataQuery {
|
||||||
|
/**
|
||||||
|
* A - Z
|
||||||
|
*/
|
||||||
refId: string;
|
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;
|
timezone: string;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
rangeRaw: RawTimeRange;
|
rangeRaw: RawTimeRange;
|
||||||
targets: DataQuery[];
|
targets: TQuery[];
|
||||||
panelId: number;
|
panelId: number;
|
||||||
dashboardId: number;
|
dashboardId: number;
|
||||||
cacheTimeout?: string;
|
cacheTimeout?: string;
|
||||||
|
|||||||
@@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap {
|
|||||||
from: string;
|
from: string;
|
||||||
to: 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 { PanelProps, PanelOptionsProps } from './panel';
|
||||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
||||||
|
|
||||||
export interface DataSourceApi {
|
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||||
name: string;
|
|
||||||
meta: PluginMeta;
|
|
||||||
pluginExports: PluginExports;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* min interval range
|
* min interval range
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +11,7 @@ export interface DataSourceApi {
|
|||||||
/**
|
/**
|
||||||
* Imports queries from a different datasource
|
* 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
|
* Initializes a datasource after instantiation
|
||||||
@@ -25,7 +21,7 @@ export interface DataSourceApi {
|
|||||||
/**
|
/**
|
||||||
* Main metrics / data query action
|
* Main metrics / data query action
|
||||||
*/
|
*/
|
||||||
query(options: DataQueryOptions): Promise<DataQueryResponse>;
|
query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test & verify datasource settings & connection details
|
* Test & verify datasource settings & connection details
|
||||||
@@ -35,20 +31,27 @@ export interface DataSourceApi {
|
|||||||
/**
|
/**
|
||||||
* Get hints for query improvements
|
* 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 {
|
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||||
datasource: DataSourceApi;
|
datasource: DSType;
|
||||||
query: DataQuery;
|
query: TQuery;
|
||||||
onExecuteQuery?: () => void;
|
onExecuteQuery?: () => void;
|
||||||
onQueryChange?: (value: DataQuery) => void;
|
onQueryChange?: (value: TQuery) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginExports {
|
export interface PluginExports {
|
||||||
Datasource?: any;
|
Datasource?: DataSourceApi;
|
||||||
QueryCtrl?: any;
|
QueryCtrl?: any;
|
||||||
QueryEditor?: ComponentClass<QueryEditorProps>;
|
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
|
||||||
ConfigCtrl?: any;
|
ConfigCtrl?: any;
|
||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
VariableQueryEditor?: any;
|
VariableQueryEditor?: any;
|
||||||
|
|||||||
@@ -21,9 +21,12 @@ export interface TimeSeriesVM {
|
|||||||
color: string;
|
color: string;
|
||||||
data: TimeSeriesValue[][];
|
data: TimeSeriesValue[][];
|
||||||
stats: TimeSeriesStats;
|
stats: TimeSeriesStats;
|
||||||
|
allIsNull: boolean;
|
||||||
|
allIsZero: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeSeriesStats {
|
export interface TimeSeriesStats {
|
||||||
|
[key: string]: number | null;
|
||||||
total: number | null;
|
total: number | null;
|
||||||
max: number | null;
|
max: number | null;
|
||||||
min: number | null;
|
min: number | null;
|
||||||
@@ -36,8 +39,6 @@ export interface TimeSeriesStats {
|
|||||||
range: number | null;
|
range: number | null;
|
||||||
timeStep: number;
|
timeStep: number;
|
||||||
count: number;
|
count: number;
|
||||||
allIsNull: boolean;
|
|
||||||
allIsZero: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NullValueMode {
|
export enum NullValueMode {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { colors } from './colors';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
timeSeries: TimeSeries[];
|
timeSeries: TimeSeries[];
|
||||||
nullValueMode: NullValueMode;
|
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 vmSeries = timeSeries.map((item, index) => {
|
||||||
const colorIndex = index % colorPalette.length;
|
const colorIndex = index % colors.length;
|
||||||
const label = item.target;
|
const label = item.target;
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
@@ -49,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof currentValue !== 'number') {
|
if (currentValue !== null && typeof currentValue !== 'number') {
|
||||||
continue;
|
throw {message: 'Time series contains non number values'};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to missing values we could have different timeStep all along the series
|
// 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 {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
label: label,
|
label: label,
|
||||||
color: colorPalette[colorIndex],
|
color: colors[colorIndex],
|
||||||
|
allIsZero,
|
||||||
|
allIsNull,
|
||||||
stats: {
|
stats: {
|
||||||
total,
|
total,
|
||||||
min,
|
min,
|
||||||
@@ -164,8 +167,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O
|
|||||||
range,
|
range,
|
||||||
count,
|
count,
|
||||||
first,
|
first,
|
||||||
allIsZero,
|
|
||||||
allIsNull,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import config from 'app/core/config';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
import { ThemeNames, ThemeName } from '@grafana/ui';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
isGrafanaAdmin: any;
|
isGrafanaAdmin: any;
|
||||||
@@ -63,6 +64,10 @@ export class ContextSrv {
|
|||||||
hasAccessToExplore() {
|
hasAccessToExplore() {
|
||||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTheme(): ThemeName {
|
||||||
|
return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextSrv = new ContextSrv();
|
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.
|
* 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 (
|
return (
|
||||||
queries &&
|
queries &&
|
||||||
queries.some(
|
queries.some(
|
||||||
@@ -280,7 +280,11 @@ export function makeTimeSeriesList(dataList) {
|
|||||||
/**
|
/**
|
||||||
* Update the query history. Side-effect: store history in local storage
|
* 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();
|
const ts = Date.now();
|
||||||
queries.forEach(query => {
|
queries.forEach(query => {
|
||||||
history = [{ query, ts }, ...history];
|
history = [{ query, ts }, ...history];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PanelResizer extends PureComponent<Props, 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;
|
prevEditorHeight: number;
|
||||||
throttledChangeHeight: (height: number) => void;
|
throttledChangeHeight: (height: number) => void;
|
||||||
throttledResizeDone: () => void;
|
throttledResizeDone: () => void;
|
||||||
|
|||||||
@@ -111,14 +111,11 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="toolbar__heading">{heading}</div>
|
<div className="toolbar__left">
|
||||||
{renderToolbar && renderToolbar()}
|
<div className="toolbar__heading">{heading}</div>
|
||||||
{toolbarItems.length > 0 && (
|
{renderToolbar && renderToolbar()}
|
||||||
<>
|
</div>
|
||||||
<div className="gf-form--grow" />
|
{toolbarItems.map(item => this.renderButton(item))}
|
||||||
{toolbarItems.map(item => this.renderButton(item))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-editor__scroll">
|
<div className="panel-editor__scroll">
|
||||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
|
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import config from 'app/core/config';
|
|||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { DashboardModel } from '../dashboard_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';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -133,14 +133,13 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||||
<div className="m-l-2">
|
<div className="flex-grow" />
|
||||||
{!isAddingMixed && (
|
{!isAddingMixed && (
|
||||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||||
Add Query
|
Add Query
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isAddingMixed && this.renderMixedPicker()}
|
{isAddingMixed && this.renderMixedPicker()}
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
target: query,
|
target: query,
|
||||||
panel: panel,
|
panel: panel,
|
||||||
refresh: () => panel.refresh(),
|
refresh: () => panel.refresh(),
|
||||||
render: () => panel.render,
|
render: () => panel.render(),
|
||||||
events: panel.events,
|
events: panel.events,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
{inMixedMode && <em className="query-editor-row__context-info"> ({datasourceName})</em>}
|
{inMixedMode && <em className="query-editor-row__context-info"> ({datasourceName})</em>}
|
||||||
{isDisabled && <em className="query-editor-row__context-info"> Disabled</em>}
|
{isDisabled && <em className="query-editor-row__context-info"> Disabled</em>}
|
||||||
</div>
|
</div>
|
||||||
<div className="query-editor-row__collapsed-text">
|
<div className="query-editor-row__collapsed-text" onClick={this.onToggleEditMode}>
|
||||||
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="query-editor-row__actions">
|
<div className="query-editor-row__actions">
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { response, isLoading } = this.state.dsQuery;
|
const { response, isLoading } = this.state.dsQuery;
|
||||||
const { isMocking } = this.state;
|
|
||||||
const openNodes = this.getNrOfOpenNodes();
|
const openNodes = this.getNrOfOpenNodes();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -199,20 +198,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
|||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMocking && <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />}
|
<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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const mustKeepProps: { [str: string]: boolean } = {
|
|||||||
hasRefreshed: true,
|
hasRefreshed: true,
|
||||||
events: true,
|
events: true,
|
||||||
cacheTimeout: true,
|
cacheTimeout: true,
|
||||||
nullPointMode: true,
|
|
||||||
cachedPluginOptions: true,
|
cachedPluginOptions: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
};
|
};
|
||||||
@@ -244,8 +243,6 @@ export class PanelModel {
|
|||||||
addQuery(query?: Partial<DataQuery>) {
|
addQuery(query?: Partial<DataQuery>) {
|
||||||
query = query || { refId: 'A' };
|
query = query || { refId: 'A' };
|
||||||
query.refId = this.getNextQueryLetter();
|
query.refId = this.getNextQueryLetter();
|
||||||
query.isNew = true;
|
|
||||||
|
|
||||||
this.targets.push(query);
|
this.targets.push(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,11 +242,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="navbar-buttons explore-first-button">
|
<>
|
||||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
<div className="navbar-page-btn" />
|
||||||
Close Split
|
<div className="navbar-buttons explore-first-button">
|
||||||
</button>
|
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||||
</div>
|
Close Split
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!datasourceMissing ? (
|
{!datasourceMissing ? (
|
||||||
<div className="navbar-buttons">
|
<div className="navbar-buttons">
|
||||||
@@ -274,7 +277,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
<div className="navbar-buttons relative">
|
<div className="navbar-buttons relative">
|
||||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
|
<button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
|
||||||
Run Query{' '}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
import { getIntervals } from 'app/core/utils/explore';
|
|
||||||
import { getTimeSrv } from 'app/features/dashboard/time_srv';
|
import { getTimeSrv } from 'app/features/dashboard/time_srv';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -37,8 +36,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
|||||||
const template = '<plugin-component type="query-ctrl"> </plugin-component>';
|
const template = '<plugin-component type="query-ctrl"> </plugin-component>';
|
||||||
const target = { datasource: datasource.name, ...initialQuery };
|
const target = { datasource: datasource.name, ...initialQuery };
|
||||||
const scopeProps = {
|
const scopeProps = {
|
||||||
target,
|
|
||||||
ctrl: {
|
ctrl: {
|
||||||
|
datasource,
|
||||||
|
target,
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
this.props.onQueryChange(target, false);
|
this.props.onQueryChange(target, false);
|
||||||
this.props.onExecuteQuery();
|
this.props.onExecuteQuery();
|
||||||
@@ -48,11 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
|||||||
datasource,
|
datasource,
|
||||||
targets: [target],
|
targets: [target],
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {},
|
||||||
getNextQueryLetter: x => '',
|
|
||||||
},
|
|
||||||
hideEditorRowActions: true,
|
|
||||||
...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
|
// Libraries
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Cascader from 'rc-cascader';
|
import Cascader from 'rc-cascader';
|
||||||
import PluginPrism from 'slate-prism';
|
import PluginPrism from 'slate-prism';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
|
|
||||||
import { DataQuery } from '@grafana/ui/src/types';
|
// Components
|
||||||
import { TypeaheadOutput } from 'app/types/explore';
|
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||||
|
|
||||||
|
// Utils & Services
|
||||||
// dom also includes Element polyfills
|
// dom also includes Element polyfills
|
||||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
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';
|
const PRISM_SYNTAX = 'promql';
|
||||||
|
|
||||||
@@ -63,10 +68,10 @@ interface LokiQueryFieldProps {
|
|||||||
error?: string | JSX.Element;
|
error?: string | JSX.Element;
|
||||||
hint?: any;
|
hint?: any;
|
||||||
history?: any[];
|
history?: any[];
|
||||||
initialQuery?: DataQuery;
|
initialQuery?: LokiQuery;
|
||||||
onClickHintFix?: (action: any) => void;
|
onClickHintFix?: (action: any) => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
onQueryChange?: (value: DataQuery, override?: boolean) => void;
|
onQueryChange?: (value: LokiQuery, override?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LokiQueryFieldState {
|
interface LokiQueryFieldState {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
|
import { LokiQuery } from './types';
|
||||||
|
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||||
|
|
||||||
describe('LokiDatasource', () => {
|
describe('LokiDatasource', () => {
|
||||||
const instanceSettings: any = {
|
const instanceSettings: any = {
|
||||||
@@ -13,12 +15,13 @@ describe('LokiDatasource', () => {
|
|||||||
replace: a => a,
|
replace: a => a,
|
||||||
};
|
};
|
||||||
|
|
||||||
const range = { from: 'now-6h', to: 'now' };
|
|
||||||
|
|
||||||
test('should use default max lines when no limit given', () => {
|
test('should use default max lines when no limit given', () => {
|
||||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||||
backendSrvMock.datasourceRequest = jest.fn();
|
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.length).toBe(1);
|
||||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
|
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
|
||||||
});
|
});
|
||||||
@@ -28,7 +31,10 @@ describe('LokiDatasource', () => {
|
|||||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
||||||
backendSrvMock.datasourceRequest = jest.fn();
|
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.length).toBe(1);
|
||||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// Services & Utils
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
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 { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||||
|
|
||||||
import LanguageProvider from './language_provider';
|
import LanguageProvider from './language_provider';
|
||||||
import { mergeStreamsToLogs } from './result_transformer';
|
import { mergeStreamsToLogs } from './result_transformer';
|
||||||
import { formatQuery, parseQuery } from './query_utils';
|
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;
|
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
|
const queryTargets = options.targets
|
||||||
.filter(target => target.expr)
|
.filter(target => target.expr)
|
||||||
.map(target => this.prepareQueryTarget(target, options));
|
.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);
|
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 || '');
|
const parsed = parseQuery(query.expr || '');
|
||||||
let selector = parsed.query;
|
let selector = parsed.query;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -124,7 +129,7 @@ export default class LokiDatasource {
|
|||||||
return { ...query, expr: expression };
|
return { ...query, expr: expression };
|
||||||
}
|
}
|
||||||
|
|
||||||
getHighlighterExpression(query: DataQuery): string {
|
getHighlighterExpression(query: LokiQuery): string {
|
||||||
return parseQuery(query.expr).regexp;
|
return parseQuery(query.expr).regexp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
// Services & Utils
|
||||||
|
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
||||||
|
import syntax from './syntax';
|
||||||
|
|
||||||
|
// Types
|
||||||
import {
|
import {
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
CompletionItemGroup,
|
CompletionItemGroup,
|
||||||
@@ -9,9 +15,7 @@ import {
|
|||||||
TypeaheadOutput,
|
TypeaheadOutput,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
import { LokiQuery } from './types';
|
||||||
import syntax from './syntax';
|
|
||||||
import { DataQuery } from '@grafana/ui/src/types';
|
|
||||||
|
|
||||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
@@ -20,7 +24,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
|||||||
|
|
||||||
const wrapLabel = (label: string) => ({ label });
|
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 cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||||
const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label);
|
const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label);
|
||||||
const count = historyForItem.length;
|
const count = historyForItem.length;
|
||||||
@@ -155,7 +161,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return { context, refresher, suggestions };
|
return { context, refresher, suggestions };
|
||||||
}
|
}
|
||||||
|
|
||||||
async importQueries(queries: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
|
async importQueries(queries: LokiQuery[], datasourceType: string): Promise<LokiQuery[]> {
|
||||||
if (datasourceType === 'prometheus') {
|
if (datasourceType === 'prometheus') {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
queries.map(async query => {
|
queries.map(async query => {
|
||||||
|
|||||||
@@ -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 BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
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 HISTOGRAM_GROUP = '__histograms__';
|
||||||
const METRIC_MARK = 'metric';
|
const METRIC_MARK = 'metric';
|
||||||
@@ -88,13 +88,13 @@ interface CascaderOption {
|
|||||||
interface PromQueryFieldProps {
|
interface PromQueryFieldProps {
|
||||||
datasource: any;
|
datasource: any;
|
||||||
error?: string | JSX.Element;
|
error?: string | JSX.Element;
|
||||||
initialQuery: DataQuery;
|
initialQuery: PromQuery;
|
||||||
hint?: any;
|
hint?: any;
|
||||||
history?: any[];
|
history?: any[];
|
||||||
metricsByPrefix?: CascaderOption[];
|
metricsByPrefix?: CascaderOption[];
|
||||||
onClickHintFix?: (action: any) => void;
|
onClickHintFix?: (action: any) => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
onQueryChange?: (value: DataQuery, override?: boolean) => void;
|
onQueryChange?: (value: PromQuery, override?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PromQueryFieldState {
|
interface PromQueryFieldState {
|
||||||
@@ -166,7 +166,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
// Send text change to parent
|
// Send text change to parent
|
||||||
const { initialQuery, onQueryChange } = this.props;
|
const { initialQuery, onQueryChange } = this.props;
|
||||||
if (onQueryChange) {
|
if (onQueryChange) {
|
||||||
const query: DataQuery = {
|
const query: PromQuery = {
|
||||||
...initialQuery,
|
...initialQuery,
|
||||||
expr: value,
|
expr: value,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,57 +1,24 @@
|
|||||||
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
// Services & Utils
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import PrometheusMetricFindQuery from './metric_find_query';
|
import PrometheusMetricFindQuery from './metric_find_query';
|
||||||
import { ResultTransformer } from './result_transformer';
|
import { ResultTransformer } from './result_transformer';
|
||||||
import PrometheusLanguageProvider from './language_provider';
|
import PrometheusLanguageProvider from './language_provider';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
import addLabelToQuery from './add_label_to_query';
|
import addLabelToQuery from './add_label_to_query';
|
||||||
import { getQueryHints } from './query_hints';
|
import { getQueryHints } from './query_hints';
|
||||||
import { expandRecordingRules } from './language_utils';
|
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';
|
import { ExploreUrlState } from 'app/types/explore';
|
||||||
|
|
||||||
export function alignRange(start, end, step) {
|
export class PrometheusDatasource implements DataSourceApi<PromQuery> {
|
||||||
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 {
|
|
||||||
type: string;
|
type: string;
|
||||||
editorSrc: string;
|
editorSrc: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -149,7 +116,7 @@ export class PrometheusDatasource {
|
|||||||
return this.templateSrv.variableExists(target.expr);
|
return this.templateSrv.variableExists(target.expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
query(options) {
|
query(options: DataQueryOptions<PromQuery>) {
|
||||||
const start = this.getPrometheusTime(options.range.from, false);
|
const start = this.getPrometheusTime(options.range.from, false);
|
||||||
const end = this.getPrometheusTime(options.range.to, true);
|
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 };
|
let state: Partial<ExploreUrlState> = { datasource: this.name };
|
||||||
if (queries && queries.length > 0) {
|
if (queries && queries.length > 0) {
|
||||||
const expandedQueries = queries.map(query => ({
|
const expandedQueries = queries.map(query => ({
|
||||||
@@ -438,7 +405,7 @@ export class PrometheusDatasource {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryHints(query: DataQuery, result: any[]) {
|
getQueryHints(query: PromQuery, result: any[]) {
|
||||||
return getQueryHints(query.expr || '', result, this);
|
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 || '';
|
let expression = query.expr || '';
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'ADD_FILTER': {
|
case 'ADD_FILTER': {
|
||||||
@@ -507,3 +474,40 @@ export class PrometheusDatasource {
|
|||||||
return this.resultTransformer.getOriginalMetricName(labelData);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { DataQuery } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
|
export interface PromQuery extends DataQuery {
|
||||||
|
expr: string;
|
||||||
|
}
|
||||||
|
|
||||||
+8
-8
@@ -10,18 +10,17 @@ import { FormLabel, Select, SelectOptionItem } from '@grafana/ui';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { QueryEditorProps } from '@grafana/ui/src/types';
|
import { QueryEditorProps } from '@grafana/ui/src/types';
|
||||||
|
import { TestDataDatasource } from './datasource';
|
||||||
interface Scenario {
|
import { TestDataQuery, Scenario } from './types';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
scenarioList: Scenario[];
|
scenarioList: Scenario[];
|
||||||
current: Scenario | null;
|
current: Scenario | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryEditor extends PureComponent<QueryEditorProps> {
|
type Props = QueryEditorProps<TestDataDatasource, TestDataQuery>;
|
||||||
|
|
||||||
|
export class QueryEditor extends PureComponent<Props> {
|
||||||
backendSrv: BackendSrv = getBackendSrv();
|
backendSrv: BackendSrv = getBackendSrv();
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
@@ -30,11 +29,12 @@ export class QueryEditor extends PureComponent<QueryEditorProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { query } = this.props;
|
const { query, datasource } = this.props;
|
||||||
|
|
||||||
query.scenarioId = query.scenarioId || 'random_walk';
|
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 });
|
const current = _.find(scenarioList, { id: query.scenarioId });
|
||||||
|
|
||||||
this.setState({ scenarioList: scenarioList, current: current });
|
this.setState({ scenarioList: scenarioList, current: current });
|
||||||
|
|||||||
+9
-4
@@ -1,15 +1,17 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
import { DataSourceApi, DataQueryOptions } from '@grafana/ui';
|
||||||
|
import { TestDataQuery, Scenario } from './types';
|
||||||
|
|
||||||
class TestDataDatasource {
|
export class TestDataDatasource implements DataSourceApi<TestDataQuery> {
|
||||||
id: any;
|
id: number;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(instanceSettings, private backendSrv, private $q) {
|
constructor(instanceSettings, private backendSrv, private $q) {
|
||||||
this.id = instanceSettings.id;
|
this.id = instanceSettings.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
query(options) {
|
query(options: DataQueryOptions<TestDataQuery>) {
|
||||||
const queries = _.filter(options.targets, item => {
|
const queries = _.filter(options.targets, item => {
|
||||||
return item.hide !== true;
|
return item.hide !== true;
|
||||||
}).map(item => {
|
}).map(item => {
|
||||||
@@ -91,6 +93,9 @@ class TestDataDatasource {
|
|||||||
message: 'Data source is working',
|
message: 'Data source is working',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScenarios(): Promise<Scenario[]> {
|
||||||
|
return this.backendSrv.get('/api/tsdb/testdata/scenarios');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TestDataDatasource };
|
|
||||||
|
|||||||
+11
@@ -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 React, { PureComponent } from 'react';
|
||||||
import { PanelProps, NullValueMode } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
// Services & Utils
|
||||||
import Gauge from 'app/viz/Gauge';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { processTimeSeries } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Gauge } from '@grafana/ui';
|
||||||
|
|
||||||
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
|
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
interface Props extends PanelProps<GaugeOptions> {}
|
interface Props extends PanelProps<GaugeOptions> {}
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<Props> {
|
export class GaugePanel extends PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
||||||
|
|
||||||
const prefix = onInterpolate(options.prefix);
|
const prefix = onInterpolate(options.prefix);
|
||||||
const suffix = onInterpolate(options.suffix);
|
const suffix = onInterpolate(options.suffix);
|
||||||
|
|
||||||
const vmSeries = getTimeSeriesVMs({
|
const vmSeries = processTimeSeries({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,6 +35,7 @@ export class GaugePanel extends PureComponent<Props> {
|
|||||||
height={height}
|
height={height}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
suffix={suffix}
|
suffix={suffix}
|
||||||
|
theme={contextSrv.getTheme()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import {
|
import {
|
||||||
BasicGaugeColor,
|
|
||||||
PanelOptionsProps,
|
PanelOptionsProps,
|
||||||
ThresholdsEditor,
|
ThresholdsEditor,
|
||||||
Threshold,
|
Threshold,
|
||||||
@@ -15,7 +14,6 @@ import { GaugeOptions } from './types';
|
|||||||
|
|
||||||
export const defaultProps = {
|
export const defaultProps = {
|
||||||
options: {
|
options: {
|
||||||
baseColor: BasicGaugeColor.Green,
|
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||||
|
|
||||||
export interface GaugeOptions {
|
export interface GaugeOptions {
|
||||||
baseColor: string;
|
|
||||||
decimals: number;
|
decimals: number;
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { colors } from '@grafana/ui';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { processTimeSeries } from '@grafana/ui/src/utils';
|
import { processTimeSeries } from '@grafana/ui/src/utils';
|
||||||
@@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent<Props> {
|
|||||||
const vmSeries = processTimeSeries({
|
const vmSeries = processTimeSeries({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Ignore,
|
||||||
colorPalette: colors,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -243,9 +243,9 @@ export interface ExploreUrlState {
|
|||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem {
|
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||||
ts: number;
|
ts: number;
|
||||||
query: DataQuery;
|
query: TQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class LanguageProvider {
|
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 {
|
.tight-form-func {
|
||||||
background: $tight-form-func-bg;
|
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 {
|
.rst-text::before {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
}
|
}
|
||||||
@@ -202,8 +138,8 @@ input[type='text'].tight-form-func-param {
|
|||||||
background: $page-bg;
|
background: $page-bg;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
.query-editor-row__ref-id {
|
.query-editor-row__ref-id {
|
||||||
font-weight: $font-weight-semi-bold;
|
font-weight: $font-weight-semi-bold;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
@@ -256,7 +192,7 @@ input[type='text'].tight-form-func-param {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-row__body {
|
.query-editor-row__body {
|
||||||
margin: 0 0 10px 40px;
|
margin: 2px 0 10px 40px;
|
||||||
background: $page-bg;
|
background: $page-bg;
|
||||||
|
|
||||||
&--collapsed {
|
&--collapsed {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar__left {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar__main {
|
.toolbar__main {
|
||||||
padding: 0 $input-padding-x;
|
padding: 0 $input-padding-x;
|
||||||
font-size: $font-size-md;
|
font-size: $font-size-md;
|
||||||
|
|||||||
@@ -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