mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge master
This commit is contained in:
commit
b4a3aecbbc
@ -5,12 +5,15 @@
|
||||
|
||||
### Minor
|
||||
* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
|
||||
* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
|
||||
* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
|
||||
|
||||
### Bug Fixes
|
||||
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
|
||||
* **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
|
||||
* **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
|
||||
* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
|
||||
* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
|
||||
|
||||
# 6.0.1 (2019-03-06)
|
||||
|
||||
|
296
devenv/dev-dashboards/panel_tests_multiseries_gauge.json
Normal file
296
devenv/dev-dashboards/panel_tests_multiseries_gauge.json
Normal file
@ -0,0 +1,296 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#1F78C1",
|
||||
"index": 5,
|
||||
"value": 96.875
|
||||
},
|
||||
{
|
||||
"color": "#E24D42",
|
||||
"index": 4,
|
||||
"value": 93.75
|
||||
},
|
||||
{
|
||||
"color": "#EF843C",
|
||||
"index": 3,
|
||||
"value": 87.5
|
||||
},
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"index": 2,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": [
|
||||
{
|
||||
"from": "50",
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Hello :) ",
|
||||
"to": "90",
|
||||
"type": 2,
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Horizontal with range variable",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 2,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Repeat horizontal",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 14,
|
||||
"w": 5,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 4,
|
||||
"links": [],
|
||||
"options-gauge": {
|
||||
"decimals": 0,
|
||||
"maxValue": "200",
|
||||
"minValue": 0,
|
||||
"options": {
|
||||
"decimals": 0,
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "avg",
|
||||
"suffix": "",
|
||||
"thresholds": [],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"prefix": "",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"stat": "max",
|
||||
"suffix": "",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "#6ED0E0",
|
||||
"index": 2,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"unit": "none",
|
||||
"valueMappings": []
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Vertical",
|
||||
"type": "gauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 17,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Multi series gauges",
|
||||
"uid": "szkuR1umk",
|
||||
"version": 7
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
|
||||
"@types/angular": "^1.6.6",
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/commander": "^2.12.2",
|
||||
|
@ -0,0 +1,54 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number, text } from '@storybook/addon-knobs';
|
||||
import { BarGauge } from './BarGauge';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
value: number('value', 70),
|
||||
minValue: number('minValue', 0),
|
||||
maxValue: number('maxValue', 100),
|
||||
threshold1Value: number('threshold1Value', 40),
|
||||
threshold1Color: text('threshold1Color', 'orange'),
|
||||
threshold2Value: number('threshold2Value', 60),
|
||||
threshold2Color: text('threshold2Color', 'red'),
|
||||
unit: text('unit', 'ms'),
|
||||
decimals: number('decimals', 1),
|
||||
};
|
||||
};
|
||||
|
||||
const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
|
||||
|
||||
BarGaugeStories.addDecorator(withCenteredStory);
|
||||
|
||||
BarGaugeStories.add('Vertical, with basic thresholds', () => {
|
||||
const {
|
||||
value,
|
||||
minValue,
|
||||
maxValue,
|
||||
threshold1Color,
|
||||
threshold2Color,
|
||||
threshold1Value,
|
||||
threshold2Value,
|
||||
unit,
|
||||
decimals,
|
||||
} = getKnobs();
|
||||
|
||||
return renderComponentWithTheme(BarGauge, {
|
||||
width: 200,
|
||||
height: 400,
|
||||
value: value,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
unit: unit,
|
||||
prefix: '',
|
||||
postfix: '',
|
||||
decimals: decimals,
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: 'green' },
|
||||
{ index: 1, value: threshold1Value, color: threshold1Color },
|
||||
{ index: 1, value: threshold2Value, color: threshold2Color },
|
||||
],
|
||||
});
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BarGauge, Props } from './BarGauge';
|
||||
import { VizOrientation } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
unit: 'none',
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme(),
|
||||
orientation: VizOrientation.Horizontal,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<BarGauge {...props} />);
|
||||
const instance = wrapper.instance() as BarGauge;
|
||||
|
||||
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.getValueColors().value).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: 10, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(instance.getValueColors().value).toEqual('#EAB839');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Render BarGauge with basic options', () => {
|
||||
it('should render', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
239
packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
Normal file
239
packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
Normal file
@ -0,0 +1,239 @@
|
||||
// Library
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
|
||||
// Types
|
||||
import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
|
||||
|
||||
const BAR_SIZE_RATIO = 0.8;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
unit: string;
|
||||
width: number;
|
||||
thresholds: Threshold[];
|
||||
valueMappings: ValueMapping[];
|
||||
value: TimeSeriesValue;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
orientation: VizOrientation;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* This visualization is still in POC state, needed more tests & better structure
|
||||
*/
|
||||
export class BarGauge extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
value: 100,
|
||||
unit: 'none',
|
||||
orientation: VizOrientation.Horizontal,
|
||||
thresholds: [],
|
||||
valueMappings: [],
|
||||
};
|
||||
|
||||
getNumericValue(): number {
|
||||
if (Number.isFinite(this.props.value as number)) {
|
||||
return this.props.value as number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getValueColors(): BarColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
return {
|
||||
value: color,
|
||||
border: color,
|
||||
bar: tinycolor(color)
|
||||
.setAlpha(0.3)
|
||||
.toRgbString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: getColorFromHexRgbOrName('gray', theme.type),
|
||||
bar: getColorFromHexRgbOrName('gray', theme.type),
|
||||
border: getColorFromHexRgbOrName('gray', theme.type),
|
||||
};
|
||||
}
|
||||
|
||||
getCellColor(positionValue: TimeSeriesValue): string {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value)) {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString();
|
||||
} else {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.7)
|
||||
.toRgbString();
|
||||
}
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
getValueStyles(value: string, color: string, width: number): CSSProperties {
|
||||
const guess = width / (value.length * 1.1);
|
||||
const fontSize = Math.min(Math.max(guess, 14), 40);
|
||||
|
||||
return {
|
||||
color: color,
|
||||
fontSize: fontSize + 'px',
|
||||
};
|
||||
}
|
||||
|
||||
renderVerticalBar(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width } = this.props;
|
||||
|
||||
const maxHeight = height * BAR_SIZE_RATIO;
|
||||
const barHeight = Math.max(valuePercent * maxHeight, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
};
|
||||
|
||||
const barStyles: CSSProperties = {
|
||||
height: `${barHeight}px`,
|
||||
width: `${width}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderTop: `1px solid ${colors.border}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
<div style={barStyles} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalBar(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width } = this.props;
|
||||
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const barWidth = Math.max(valuePercent * maxWidth, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
|
||||
valueStyles.marginLeft = '8px';
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const barStyles = {
|
||||
height: `${height}px`,
|
||||
width: `${barWidth}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderRight: `1px solid ${colors.border}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={barStyles} />
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width, maxValue, minValue } = this.props;
|
||||
|
||||
const valueRange = maxValue - minValue;
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const cellSpacing = 4;
|
||||
const cellCount = 30;
|
||||
const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
valueStyles.marginLeft = '8px';
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const cells: JSX.Element[] = [];
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const currentValue = (valueRange / cellCount) * i;
|
||||
const cellColor = this.getCellColor(currentValue);
|
||||
const cellStyles: CSSProperties = {
|
||||
width: `${cellWidth}px`,
|
||||
backgroundColor: cellColor,
|
||||
marginRight: '4px',
|
||||
height: `${height}px`,
|
||||
borderRadius: '2px',
|
||||
};
|
||||
|
||||
cells.push(<div style={cellStyles} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
{cells}
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { maxValue, minValue, orientation, unit, decimals } = this.props;
|
||||
|
||||
const numericValue = this.getNumericValue();
|
||||
const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
|
||||
|
||||
const formatFunc = getValueFormat(unit);
|
||||
const valueFormatted = formatFunc(numericValue, decimals);
|
||||
const vertical = orientation === 'vertical';
|
||||
|
||||
return vertical
|
||||
? this.renderVerticalBar(valueFormatted, valuePercent)
|
||||
: this.renderHorizontalLCD(valueFormatted, valuePercent);
|
||||
}
|
||||
}
|
||||
|
||||
interface BarColors {
|
||||
value: string;
|
||||
bar: string;
|
||||
border: string;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
.bar-gauge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bar-gauge__value {
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,358 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render BarGauge with basic options should render 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"height": "300px",
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="bar-gauge__value"
|
||||
style={
|
||||
Object {
|
||||
"color": "#7EB26D",
|
||||
"fontSize": "27.272727272727263px",
|
||||
"marginLeft": "8px",
|
||||
}
|
||||
}
|
||||
>
|
||||
25
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -50,7 +50,16 @@ ColorPickerStories.add('Series color picker', () => {
|
||||
color={selectedColor}
|
||||
onChange={color => updateSelectedColor(color)}
|
||||
>
|
||||
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
|
||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
onMouseLeave={hideColorPicker}
|
||||
onClick={showColorPicker}
|
||||
style={{ color: selectedColor, cursor: 'pointer' }}
|
||||
>
|
||||
Open color picker
|
||||
</div>
|
||||
)}
|
||||
</SeriesColorPicker>
|
||||
);
|
||||
}}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { ColorPickerTrigger } from './ColorPickerTrigger';
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
it('renders ColorPickerTrigger component by default', () => {
|
||||
expect(
|
||||
renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorPickerTrigger)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders custom trigger when supplied', () => {
|
||||
const div = renderer
|
||||
.create(
|
||||
<ColorPicker color="#EAB839" onChange={() => {}}>
|
||||
{() => <div>Custom trigger</div>}
|
||||
</ColorPicker>
|
||||
)
|
||||
.root.findByType('div');
|
||||
expect(div.children[0]).toBe('Custom trigger');
|
||||
});
|
||||
});
|
@ -1,4 +1,5 @@
|
||||
import React, { Component, createRef } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { PopperController } from '../Tooltip/PopperController';
|
||||
import { Popper } from '../Tooltip/Popper';
|
||||
import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
|
||||
@ -6,14 +7,29 @@ import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
import { ColorPickerTrigger } from './ColorPickerTrigger';
|
||||
|
||||
/**
|
||||
* If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function
|
||||
* as a child. You will get show/hide function which you can map to desired interaction (like onClick or onMouseLeave)
|
||||
* and a ref which needs to be passed to an HTMLElement for correct positioning. If you want to use class or functional
|
||||
* component as a custom trigger you will need to forward the reference to first HTMLElement child.
|
||||
*/
|
||||
type ColorPickerTriggerRenderer = (props: {
|
||||
// This should be a React.RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
|
||||
// to a specific type like React.RefObject<HTMLDivElement> even though it would be fine in runtime.
|
||||
ref: React.RefObject<any>;
|
||||
showColorPicker: () => void;
|
||||
hideColorPicker: () => void;
|
||||
}) => React.ReactNode;
|
||||
|
||||
export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<T>,
|
||||
displayName = 'ColorPicker'
|
||||
) => {
|
||||
return class ColorPicker extends Component<T, any> {
|
||||
return class ColorPicker extends Component<T & { children?: ColorPickerTriggerRenderer }, any> {
|
||||
static displayName = displayName;
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
pickerTriggerRef = createRef<any>();
|
||||
|
||||
onColorChange = (color: string) => {
|
||||
const { onColorChange, onChange } = this.props;
|
||||
@ -23,11 +39,11 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, children } = this.props;
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...this.props,
|
||||
...omit(this.props, 'children'),
|
||||
onChange: this.onColorChange,
|
||||
});
|
||||
const { theme, children } = this.props;
|
||||
|
||||
return (
|
||||
<PopperController content={popoverElement} hideAfter={300}>
|
||||
@ -45,27 +61,21 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
)}
|
||||
|
||||
{children ? (
|
||||
React.cloneElement(children as JSX.Element, {
|
||||
// Children have a bit weird type due to intersection used in the definition so we need to cast here,
|
||||
// but the definition is correct and should not allow to pass a children that does not conform to
|
||||
// ColorPickerTriggerRenderer type.
|
||||
(children as ColorPickerTriggerRenderer)({
|
||||
ref: this.pickerTriggerRef,
|
||||
onClick: showPopper,
|
||||
onMouseLeave: hidePopper,
|
||||
showColorPicker: showPopper,
|
||||
hideColorPicker: hidePopper,
|
||||
})
|
||||
) : (
|
||||
<div
|
||||
<ColorPickerTrigger
|
||||
ref={this.pickerTriggerRef}
|
||||
onClick={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
className="sp-replacer sp-light"
|
||||
>
|
||||
<div className="sp-preview">
|
||||
<div
|
||||
className="sp-preview-inner"
|
||||
style={{
|
||||
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
color={getColorFromHexRgbOrName(this.props.color || '#000000', theme.type)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -17,8 +17,8 @@ export interface ColorPickerProps extends Themeable {
|
||||
*/
|
||||
onColorChange?: ColorPickerChangeHandler;
|
||||
enableNamedColors?: boolean;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export interface Props<T> extends ColorPickerProps, PopperContentProps {
|
||||
customPickers?: T;
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
interface ColorPickerTriggerProps {
|
||||
onClick: () => void;
|
||||
onMouseLeave: () => void;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger(
|
||||
props: ColorPickerTriggerProps,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={props.onClick}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
background: 'inherit',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
padding: 0,
|
||||
borderRadius: 10,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 15,
|
||||
height: 15,
|
||||
border: 'none',
|
||||
margin: 0,
|
||||
float: 'left',
|
||||
zIndex: 0,
|
||||
backgroundImage:
|
||||
// tslint:disable-next-line:max-line-length
|
||||
'url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: props.color,
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -161,59 +161,6 @@ $arrowSize: 15px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sp-replacer {
|
||||
background: inherit;
|
||||
border: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sp-replacer:hover,
|
||||
.sp-replacer.sp-active {
|
||||
border-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sp-container {
|
||||
border-radius: 0;
|
||||
background-color: $dropdownBackground;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sp-palette-container,
|
||||
.sp-picker-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sp-dd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sp-preview {
|
||||
position: relative;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
float: left;
|
||||
z-index: 0;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
.sp-preview-inner,
|
||||
.sp-alpha-inner,
|
||||
.sp-thumb-inner {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.gf-color-picker__body {
|
||||
padding-bottom: $arrowSize;
|
||||
padding-left: 6px;
|
||||
|
@ -1,5 +1,5 @@
|
||||
.form-field {
|
||||
margin-bottom: $gf-form-margin;
|
||||
margin-bottom: $space-xxs;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { Themeable, GrafanaThemeType } from '../../types/theme';
|
||||
import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
|
||||
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||
|
||||
type TimeSeriesValue = string | number | null;
|
||||
import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
import { Themeable } from '../../index';
|
||||
|
||||
type GaugeValue = string | number | null;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
decimals?: number | null;
|
||||
@ -30,7 +30,7 @@ const FONT_SCALE = 1;
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
@ -41,7 +41,6 @@ export class Gauge extends PureComponent<Props> {
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
theme: GrafanaThemeType.Dark,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -52,7 +51,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatValue(value: TimeSeriesValue) {
|
||||
formatValue(value: GaugeValue) {
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
if (isNaN(value as number)) {
|
||||
@ -73,26 +72,16 @@ export class Gauge extends PureComponent<Props> {
|
||||
return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
|
||||
}
|
||||
|
||||
getFontColor(value: TimeSeriesValue) {
|
||||
getFontColor(value: GaugeValue): string {
|
||||
const { thresholds, theme } = this.props;
|
||||
|
||||
if (thresholds.length === 1) {
|
||||
return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return getColorFromHexRgbOrName(atThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
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 getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
return BasicGaugeColor.Red;
|
||||
return '';
|
||||
}
|
||||
|
||||
getFormattedThresholds() {
|
||||
@ -134,7 +123,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const options = {
|
||||
const options: any = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
@ -184,19 +173,15 @@ export class Gauge extends PureComponent<Props> {
|
||||
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>
|
||||
<div
|
||||
style={{
|
||||
height: `${Math.min(height, width * 1.3)}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Gauge;
|
||||
|
@ -3,7 +3,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
|
||||
@mixin select-control() {
|
||||
width: 100%;
|
||||
margin-right: $gf-form-margin;
|
||||
margin-right: $space-xs;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
97
packages/grafana-ui/src/components/Table/Table.story.tsx
Normal file
97
packages/grafana-ui/src/components/Table/Table.story.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
// import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Table } from './Table';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
|
||||
import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index';
|
||||
import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
|
||||
import { number, boolean } from '@storybook/addon-knobs';
|
||||
|
||||
const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
|
||||
if (scopedVars) {
|
||||
// For testing variables replacement in link
|
||||
for (const key in scopedVars) {
|
||||
const val = scopedVars[key];
|
||||
value = value.replace('$' + key, val.value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export function columnIndexToLeter(column: number) {
|
||||
const A = 'A'.charCodeAt(0);
|
||||
const c1 = Math.floor(column / 26);
|
||||
const c2 = column % 26;
|
||||
if (c1 > 0) {
|
||||
return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2);
|
||||
}
|
||||
return String.fromCharCode(A + c2);
|
||||
}
|
||||
|
||||
export function makeDummyTable(columnCount: number, rowCount: number): TableData {
|
||||
return {
|
||||
columns: Array.from(new Array(columnCount), (x, i) => {
|
||||
return {
|
||||
text: columnIndexToLeter(i),
|
||||
};
|
||||
}),
|
||||
rows: Array.from(new Array(rowCount), (x, rowId) => {
|
||||
const suffix = (rowId + 1).toString();
|
||||
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
storiesOf('Alpha/Table', module)
|
||||
.add('Basic Table', () => {
|
||||
// NOTE: This example does not seem to survice rotate &
|
||||
// Changing fixed headers... but the next one does?
|
||||
// perhaps `simpleTable` is static and reused?
|
||||
|
||||
const showHeader = boolean('Show Header', true);
|
||||
const fixedHeader = boolean('Fixed Header', true);
|
||||
const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false });
|
||||
const rotate = boolean('Rotate', false);
|
||||
|
||||
return withFullSizeStory(Table, {
|
||||
styles: [],
|
||||
data: simpleTable,
|
||||
replaceVariables,
|
||||
showHeader,
|
||||
fixedHeader,
|
||||
fixedColumns,
|
||||
rotate,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
})
|
||||
.add('Variable Size', () => {
|
||||
const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false });
|
||||
const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false });
|
||||
|
||||
const showHeader = boolean('Show Header', true);
|
||||
const fixedHeader = boolean('Fixed Header', true);
|
||||
const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false });
|
||||
const rotate = boolean('Rotate', false);
|
||||
|
||||
return withFullSizeStory(Table, {
|
||||
styles: [],
|
||||
data: makeDummyTable(columnCount, rowCount),
|
||||
replaceVariables,
|
||||
showHeader,
|
||||
fixedHeader,
|
||||
fixedColumns,
|
||||
rotate,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
})
|
||||
.add('Test Config (migrated)', () => {
|
||||
return withFullSizeStory(Table, {
|
||||
styles: migratedTestStyles,
|
||||
data: migratedTestTable,
|
||||
replaceVariables,
|
||||
showHeader: true,
|
||||
rotate: true,
|
||||
theme: getTheme(GrafanaThemeType.Light),
|
||||
});
|
||||
});
|
287
packages/grafana-ui/src/components/Table/Table.tsx
Normal file
287
packages/grafana-ui/src/components/Table/Table.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import {
|
||||
SortDirectionType,
|
||||
SortIndicator,
|
||||
MultiGrid,
|
||||
CellMeasurerCache,
|
||||
CellMeasurer,
|
||||
GridCellProps,
|
||||
} from 'react-virtualized';
|
||||
import { Themeable } from '../../types/theme';
|
||||
|
||||
import { sortTableData } from '../../utils/processTableData';
|
||||
|
||||
import { TableData, InterpolateFunction } from '@grafana/ui';
|
||||
import {
|
||||
TableCellBuilder,
|
||||
ColumnStyle,
|
||||
getCellBuilder,
|
||||
TableCellBuilderOptions,
|
||||
simpleCellBuilder,
|
||||
} from './TableCellBuilder';
|
||||
import { stringToJsRegex } from '../../utils/index';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
data: TableData;
|
||||
|
||||
showHeader: boolean;
|
||||
fixedHeader: boolean;
|
||||
fixedColumns: number;
|
||||
rotate: boolean;
|
||||
styles: ColumnStyle[];
|
||||
|
||||
replaceVariables: InterpolateFunction;
|
||||
width: number;
|
||||
height: number;
|
||||
isUTC?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
sortBy?: number;
|
||||
sortDirection?: SortDirectionType;
|
||||
data: TableData;
|
||||
}
|
||||
|
||||
interface ColumnRenderInfo {
|
||||
header: string;
|
||||
builder: TableCellBuilder;
|
||||
}
|
||||
|
||||
interface DataIndex {
|
||||
column: number;
|
||||
row: number; // -1 is the header!
|
||||
}
|
||||
|
||||
export class Table extends Component<Props, State> {
|
||||
renderer: ColumnRenderInfo[];
|
||||
measurer: CellMeasurerCache;
|
||||
scrollToTop = false;
|
||||
|
||||
static defaultProps = {
|
||||
showHeader: true,
|
||||
fixedHeader: true,
|
||||
fixedColumns: 0,
|
||||
rotate: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: props.data,
|
||||
};
|
||||
|
||||
this.renderer = this.initColumns(props);
|
||||
this.measurer = new CellMeasurerCache({
|
||||
defaultHeight: 30,
|
||||
defaultWidth: 150,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { data, styles, showHeader } = this.props;
|
||||
const { sortBy, sortDirection } = this.state;
|
||||
const dataChanged = data !== prevProps.data;
|
||||
const configsChanged =
|
||||
showHeader !== prevProps.showHeader ||
|
||||
this.props.rotate !== prevProps.rotate ||
|
||||
this.props.fixedColumns !== prevProps.fixedColumns ||
|
||||
this.props.fixedHeader !== prevProps.fixedHeader;
|
||||
|
||||
// Reset the size cache
|
||||
if (dataChanged || configsChanged) {
|
||||
this.measurer.clearAll();
|
||||
}
|
||||
|
||||
// Update the renderer if options change
|
||||
// We only *need* do to this if the header values changes, but this does every data update
|
||||
if (dataChanged || styles !== prevProps.styles) {
|
||||
this.renderer = this.initColumns(this.props);
|
||||
}
|
||||
|
||||
// Update the data when data or sort changes
|
||||
if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
|
||||
this.scrollToTop = true;
|
||||
this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') });
|
||||
}
|
||||
}
|
||||
|
||||
/** Given the configuration, setup how each column gets rendered */
|
||||
initColumns(props: Props): ColumnRenderInfo[] {
|
||||
const { styles, data } = props;
|
||||
|
||||
return data.columns.map((col, index) => {
|
||||
let title = col.text;
|
||||
let style: ColumnStyle | null = null; // ColumnStyle
|
||||
|
||||
// Find the style based on the text
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const s = styles[i];
|
||||
const regex = stringToJsRegex(s.pattern);
|
||||
if (title.match(regex)) {
|
||||
style = s;
|
||||
if (s.alias) {
|
||||
title = title.replace(regex, s.alias);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
header: title,
|
||||
builder: getCellBuilder(col, style, this.props),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
doSort = (columnIndex: number) => {
|
||||
let sort: any = this.state.sortBy;
|
||||
let dir = this.state.sortDirection;
|
||||
if (sort !== columnIndex) {
|
||||
dir = 'DESC';
|
||||
sort = columnIndex;
|
||||
} else if (dir === 'DESC') {
|
||||
dir = 'ASC';
|
||||
} else {
|
||||
sort = null;
|
||||
}
|
||||
this.setState({ sortBy: sort, sortDirection: dir });
|
||||
};
|
||||
|
||||
/** Converts the grid coordinates to TableData coordinates */
|
||||
getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
|
||||
const { showHeader, rotate } = this.props;
|
||||
const rowOffset = showHeader ? -1 : 0;
|
||||
|
||||
if (rotate) {
|
||||
return { column: rowIndex, row: columnIndex + rowOffset };
|
||||
} else {
|
||||
return { column: columnIndex, row: rowIndex + rowOffset };
|
||||
}
|
||||
};
|
||||
|
||||
onCellClick = (rowIndex: number, columnIndex: number) => {
|
||||
const { row, column } = this.getCellRef(rowIndex, columnIndex);
|
||||
if (row < 0) {
|
||||
this.doSort(column);
|
||||
} else {
|
||||
const values = this.state.data.rows[row];
|
||||
const value = values[column];
|
||||
console.log('CLICK', value, row);
|
||||
}
|
||||
};
|
||||
|
||||
headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
|
||||
const { data, sortBy, sortDirection } = this.state;
|
||||
const { columnIndex, rowIndex, style } = cell.props;
|
||||
const { column } = this.getCellRef(rowIndex, columnIndex);
|
||||
|
||||
let col = data.columns[column];
|
||||
const sorting = sortBy === column;
|
||||
if (!col) {
|
||||
col = {
|
||||
text: '??' + columnIndex + '???',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
|
||||
{col.text}
|
||||
{sorting && <SortIndicator sortDirection={sortDirection} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getTableCellBuilder = (column: number): TableCellBuilder => {
|
||||
const render = this.renderer[column];
|
||||
if (render && render.builder) {
|
||||
return render.builder;
|
||||
}
|
||||
return simpleCellBuilder; // the default
|
||||
};
|
||||
|
||||
cellRenderer = (props: GridCellProps): React.ReactNode => {
|
||||
const { rowIndex, columnIndex, key, parent } = props;
|
||||
const { row, column } = this.getCellRef(rowIndex, columnIndex);
|
||||
const { data } = this.state;
|
||||
|
||||
const isHeader = row < 0;
|
||||
const rowData = isHeader ? data.columns : data.rows[row];
|
||||
const value = rowData ? rowData[column] : '';
|
||||
const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
|
||||
|
||||
return (
|
||||
<CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
|
||||
{builder({
|
||||
value,
|
||||
row: rowData,
|
||||
column: data.columns[column],
|
||||
table: this,
|
||||
props,
|
||||
})}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
|
||||
const { data } = this.state;
|
||||
|
||||
let columnCount = data.columns.length;
|
||||
let rowCount = data.rows.length + (showHeader ? 1 : 0);
|
||||
|
||||
let fixedColumnCount = Math.min(fixedColumns, columnCount);
|
||||
let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
|
||||
|
||||
if (rotate) {
|
||||
const temp = columnCount;
|
||||
columnCount = rowCount;
|
||||
rowCount = temp;
|
||||
|
||||
fixedRowCount = 0;
|
||||
fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
|
||||
}
|
||||
|
||||
// Called after sort or the data changes
|
||||
const scroll = this.scrollToTop ? 1 : -1;
|
||||
const scrollToRow = rotate ? -1 : scroll;
|
||||
const scrollToColumn = rotate ? scroll : -1;
|
||||
if (this.scrollToTop) {
|
||||
this.scrollToTop = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiGrid
|
||||
{
|
||||
...this.state /** Force MultiGrid to update when data changes */
|
||||
}
|
||||
{
|
||||
...this.props /** Force MultiGrid to update when data changes */
|
||||
}
|
||||
scrollToRow={scrollToRow}
|
||||
columnCount={columnCount}
|
||||
scrollToColumn={scrollToColumn}
|
||||
rowCount={rowCount}
|
||||
overscanColumnCount={8}
|
||||
overscanRowCount={8}
|
||||
columnWidth={this.measurer.columnWidth}
|
||||
deferredMeasurementCache={this.measurer}
|
||||
cellRenderer={this.cellRenderer}
|
||||
rowHeight={this.measurer.rowHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
fixedColumnCount={fixedColumnCount}
|
||||
fixedRowCount={fixedRowCount}
|
||||
classNameTopLeftGrid="gf-table-fixed-column"
|
||||
classNameBottomLeftGrid="gf-table-fixed-column"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Table;
|
291
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
Normal file
291
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { GridCellProps } from 'react-virtualized';
|
||||
import { Table, Props } from './Table';
|
||||
import moment from 'moment';
|
||||
import { ValueFormatter } from '../../utils/index';
|
||||
import { GrafanaTheme } from '../../types/theme';
|
||||
import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui';
|
||||
import { InterpolateFunction } from '../../types/panel';
|
||||
|
||||
export interface TableCellBuilderOptions {
|
||||
value: any;
|
||||
column?: Column;
|
||||
row?: any[];
|
||||
table?: Table;
|
||||
className?: string;
|
||||
props: GridCellProps;
|
||||
}
|
||||
|
||||
export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>;
|
||||
|
||||
/** Simplest cell that just spits out the value */
|
||||
export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => {
|
||||
const { props, value, className } = cell;
|
||||
const { style } = props;
|
||||
|
||||
return (
|
||||
<div style={style} className={'gf-table-cell ' + className}>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ***************************************************************************
|
||||
// HERE BE DRAGONS!!!
|
||||
// ***************************************************************************
|
||||
//
|
||||
// The following code has been migrated blindy two times from the angular
|
||||
// table panel. I don't understand all the options nor do I know if they
|
||||
// are correct!
|
||||
//
|
||||
// ***************************************************************************
|
||||
|
||||
// Made to match the existing (untyped) settings in the angular table
|
||||
export interface ColumnStyle {
|
||||
pattern: string;
|
||||
|
||||
alias?: string;
|
||||
colorMode?: 'cell' | 'value';
|
||||
colors?: any[];
|
||||
decimals?: number;
|
||||
thresholds?: any[];
|
||||
type?: 'date' | 'number' | 'string' | 'hidden';
|
||||
unit?: string;
|
||||
dateFormat?: string;
|
||||
sanitize?: boolean; // not used in react
|
||||
mappingType?: any;
|
||||
valueMaps?: any;
|
||||
rangeMaps?: any;
|
||||
|
||||
link?: any;
|
||||
linkUrl?: any;
|
||||
linkTooltip?: any;
|
||||
linkTargetBlank?: boolean;
|
||||
|
||||
preserveFormat?: boolean;
|
||||
}
|
||||
|
||||
// private mapper:ValueMapper,
|
||||
// private style:ColumnStyle,
|
||||
// private theme:GrafanaTheme,
|
||||
// private column:Column,
|
||||
// private replaceVariables: InterpolateFunction,
|
||||
// private fmt?:ValueFormatter) {
|
||||
|
||||
export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder {
|
||||
if (!style) {
|
||||
return simpleCellBuilder;
|
||||
}
|
||||
|
||||
if (style.type === 'hidden') {
|
||||
// TODO -- for hidden, we either need to:
|
||||
// 1. process the Table and remove hidden fields
|
||||
// 2. do special math to pick the right column skipping hidden fields
|
||||
throw new Error('hidden not supported!');
|
||||
}
|
||||
|
||||
if (style.type === 'date') {
|
||||
return new CellBuilderWithStyle(
|
||||
(v: any) => {
|
||||
if (v === undefined || v === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (_.isArray(v)) {
|
||||
v = v[0];
|
||||
}
|
||||
let date = moment(v);
|
||||
if (false) {
|
||||
// TODO?????? this.props.isUTC) {
|
||||
date = date.utc();
|
||||
}
|
||||
return date.format(style.dateFormat);
|
||||
},
|
||||
style,
|
||||
props.theme,
|
||||
schema,
|
||||
props.replaceVariables
|
||||
).build;
|
||||
}
|
||||
|
||||
if (style.type === 'string') {
|
||||
return new CellBuilderWithStyle(
|
||||
(v: any) => {
|
||||
if (_.isArray(v)) {
|
||||
v = v.join(', ');
|
||||
}
|
||||
return v;
|
||||
},
|
||||
style,
|
||||
props.theme,
|
||||
schema,
|
||||
props.replaceVariables
|
||||
).build;
|
||||
// TODO!!!! all the mapping stuff!!!!
|
||||
}
|
||||
|
||||
if (style.type === 'number') {
|
||||
const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
|
||||
return new CellBuilderWithStyle(
|
||||
(v: any) => {
|
||||
if (v === null || v === void 0) {
|
||||
return '-';
|
||||
}
|
||||
return v;
|
||||
},
|
||||
style,
|
||||
props.theme,
|
||||
schema,
|
||||
props.replaceVariables,
|
||||
valueFormatter
|
||||
).build;
|
||||
}
|
||||
|
||||
return simpleCellBuilder;
|
||||
}
|
||||
|
||||
type ValueMapper = (value: any) => any;
|
||||
|
||||
// Runs the value through a formatter and adds colors to the cell properties
|
||||
class CellBuilderWithStyle {
|
||||
constructor(
|
||||
private mapper: ValueMapper,
|
||||
private style: ColumnStyle,
|
||||
private theme: GrafanaTheme,
|
||||
private column: Column,
|
||||
private replaceVariables: InterpolateFunction,
|
||||
private fmt?: ValueFormatter
|
||||
) {
|
||||
//
|
||||
console.log('COLUMN', column.text, theme);
|
||||
}
|
||||
|
||||
getColorForValue = (value: any): string | null => {
|
||||
const { thresholds, colors } = this.style;
|
||||
if (!thresholds || !colors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = thresholds.length; i > 0; i--) {
|
||||
if (value >= thresholds[i - 1]) {
|
||||
return getColorFromHexRgbOrName(colors[i], this.theme.type);
|
||||
}
|
||||
}
|
||||
return getColorFromHexRgbOrName(_.first(colors), this.theme.type);
|
||||
};
|
||||
|
||||
build = (cell: TableCellBuilderOptions) => {
|
||||
let { props } = cell;
|
||||
let value = this.mapper(cell.value);
|
||||
|
||||
if (_.isNumber(value)) {
|
||||
if (this.fmt) {
|
||||
value = this.fmt(value, this.style.decimals);
|
||||
}
|
||||
|
||||
// For numeric values set the color
|
||||
const { colorMode } = this.style;
|
||||
if (colorMode) {
|
||||
const color = this.getColorForValue(Number(value));
|
||||
if (color) {
|
||||
if (colorMode === 'cell') {
|
||||
props = {
|
||||
...props,
|
||||
style: {
|
||||
...props.style,
|
||||
backgroundColor: color,
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
} else if (colorMode === 'value') {
|
||||
props = {
|
||||
...props,
|
||||
style: {
|
||||
...props.style,
|
||||
color: color,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cellClasses = [];
|
||||
if (this.style.preserveFormat) {
|
||||
cellClasses.push('table-panel-cell-pre');
|
||||
}
|
||||
|
||||
if (this.style.link) {
|
||||
// Render cell as link
|
||||
const { row } = cell;
|
||||
|
||||
const scopedVars: any = {};
|
||||
if (row) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
scopedVars[`__cell_${i}`] = { value: row[i] };
|
||||
}
|
||||
}
|
||||
scopedVars['__cell'] = { value: value };
|
||||
|
||||
const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent);
|
||||
const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars);
|
||||
const cellTarget = this.style.linkTargetBlank ? '_blank' : '';
|
||||
|
||||
cellClasses.push('table-panel-cell-link');
|
||||
value = (
|
||||
<a
|
||||
href={cellLink}
|
||||
target={cellTarget}
|
||||
data-link-tooltip
|
||||
data-original-title={cellLinkTooltip}
|
||||
data-placement="right"
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// ??? I don't think this will still work!
|
||||
if (this.column.filterable) {
|
||||
cellClasses.push('table-panel-cell-filterable');
|
||||
value = (
|
||||
<>
|
||||
{value}
|
||||
<span>
|
||||
<a
|
||||
className="table-panel-filter-link"
|
||||
data-link-tooltip
|
||||
data-original-title="Filter out value"
|
||||
data-placement="bottom"
|
||||
data-row={props.rowIndex}
|
||||
data-column={props.columnIndex}
|
||||
data-operator="!="
|
||||
>
|
||||
<i className="fa fa-search-minus" />
|
||||
</a>
|
||||
<a
|
||||
className="table-panel-filter-link"
|
||||
data-link-tooltip
|
||||
data-original-title="Filter for value"
|
||||
data-placement="bottom"
|
||||
data-row={props.rowIndex}
|
||||
data-column={props.columnIndex}
|
||||
data-operator="="
|
||||
>
|
||||
<i className="fa fa-search-plus" />
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let className;
|
||||
if (cellClasses.length) {
|
||||
className = cellClasses.join(' ');
|
||||
}
|
||||
|
||||
return simpleCellBuilder({ value, props, className });
|
||||
};
|
||||
}
|
80
packages/grafana-ui/src/components/Table/_Table.scss
Normal file
80
packages/grafana-ui/src/components/Table/_Table.scss
Normal file
@ -0,0 +1,80 @@
|
||||
// .ReactVirtualized__Table {
|
||||
// }
|
||||
|
||||
// .ReactVirtualized__Table__Grid {
|
||||
// }
|
||||
|
||||
.ReactVirtualized__Table__headerRow {
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: left;
|
||||
}
|
||||
.ReactVirtualized__Table__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
border-bottom: 2px solid $body-bg;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerTruncatedText {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerColumn,
|
||||
.ReactVirtualized__Table__rowColumn {
|
||||
margin-right: 10px;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__headerColumn:first-of-type,
|
||||
.ReactVirtualized__Table__rowColumn:first-of-type {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.ReactVirtualized__Table__sortableHeaderColumn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ReactVirtualized__Table__sortableHeaderIconContainer {
|
||||
align-items: center;
|
||||
}
|
||||
.ReactVirtualized__Table__sortableHeaderIcon {
|
||||
flex: 0 0 24px;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.gf-table-header {
|
||||
padding: 3px 10px;
|
||||
|
||||
background: $list-item-bg;
|
||||
border-top: 2px solid $body-bg;
|
||||
border-bottom: 2px solid $body-bg;
|
||||
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.gf-table-cell {
|
||||
padding: 3px 10px;
|
||||
|
||||
background: $page-gradient;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-right: 2px solid $body-bg;
|
||||
border-bottom: 2px solid $body-bg;
|
||||
}
|
||||
|
||||
.gf-table-fixed-column {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
167
packages/grafana-ui/src/components/Table/examples.ts
Normal file
167
packages/grafana-ui/src/components/Table/examples.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { TableData } from '../../types/data';
|
||||
import { ColumnStyle } from './TableCellBuilder';
|
||||
|
||||
import { getColorDefinitionByName } from '@grafana/ui';
|
||||
|
||||
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
|
||||
|
||||
export const migratedTestTable = {
|
||||
type: 'table',
|
||||
columns: [
|
||||
{ text: 'Time' },
|
||||
{ text: 'Value' },
|
||||
{ text: 'Colored' },
|
||||
{ text: 'Undefined' },
|
||||
{ text: 'String' },
|
||||
{ text: 'United', unit: 'bps' },
|
||||
{ text: 'Sanitized' },
|
||||
{ text: 'Link' },
|
||||
{ text: 'Array' },
|
||||
{ text: 'Mapping' },
|
||||
{ text: 'RangeMapping' },
|
||||
{ text: 'MappingColored' },
|
||||
{ text: 'RangeMappingColored' },
|
||||
],
|
||||
rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
|
||||
} as TableData;
|
||||
|
||||
export const migratedTestStyles: ColumnStyle[] = [
|
||||
{
|
||||
pattern: 'Time',
|
||||
type: 'date',
|
||||
alias: 'Timestamp',
|
||||
},
|
||||
{
|
||||
pattern: '/(Val)ue/',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 3,
|
||||
alias: '$1',
|
||||
},
|
||||
{
|
||||
pattern: 'Colored',
|
||||
type: 'number',
|
||||
unit: 'none',
|
||||
decimals: 1,
|
||||
colorMode: 'value',
|
||||
thresholds: [50, 80],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
{
|
||||
pattern: 'String',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
pattern: 'String',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
pattern: 'United',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 2,
|
||||
},
|
||||
{
|
||||
pattern: 'Sanitized',
|
||||
type: 'string',
|
||||
sanitize: true,
|
||||
},
|
||||
{
|
||||
pattern: 'Link',
|
||||
type: 'string',
|
||||
link: true,
|
||||
linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2',
|
||||
linkTooltip: '$__cell $__cell_1 $__cell_6',
|
||||
linkTargetBlank: true,
|
||||
},
|
||||
{
|
||||
pattern: 'Array',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 3,
|
||||
},
|
||||
{
|
||||
pattern: 'Mapping',
|
||||
type: 'string',
|
||||
mappingType: 1,
|
||||
valueMaps: [
|
||||
{
|
||||
value: '1',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
text: 'off',
|
||||
},
|
||||
{
|
||||
value: 'HELLO WORLD',
|
||||
text: 'HELLO GRAFANA',
|
||||
},
|
||||
{
|
||||
value: 'value1, value2',
|
||||
text: 'value3, value4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: 'RangeMapping',
|
||||
type: 'string',
|
||||
mappingType: 2,
|
||||
rangeMaps: [
|
||||
{
|
||||
from: '1',
|
||||
to: '3',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
from: '3',
|
||||
to: '6',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: 'MappingColored',
|
||||
type: 'string',
|
||||
mappingType: 1,
|
||||
valueMaps: [
|
||||
{
|
||||
value: '1',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
colorMode: 'value',
|
||||
thresholds: [1, 2],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
{
|
||||
pattern: 'RangeMappingColored',
|
||||
type: 'string',
|
||||
mappingType: 2,
|
||||
rangeMaps: [
|
||||
{
|
||||
from: '1',
|
||||
to: '3',
|
||||
text: 'on',
|
||||
},
|
||||
{
|
||||
from: '3',
|
||||
to: '6',
|
||||
text: 'off',
|
||||
},
|
||||
],
|
||||
colorMode: 'value',
|
||||
thresholds: [2, 5],
|
||||
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
|
||||
},
|
||||
];
|
||||
|
||||
export const simpleTable = {
|
||||
type: 'table',
|
||||
columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }],
|
||||
rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]],
|
||||
};
|
@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
.thresholds-row-input-inner-value > input {
|
||||
height: $gf-form-input-height;
|
||||
height: $input-height;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
width: 150px;
|
||||
border-top: 1px solid $input-label-border-color;
|
||||
@ -86,7 +86,6 @@
|
||||
|
||||
.thresholds-row-input-inner-color-colorpicker {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
@ -96,7 +95,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: $gf-form-input-height;
|
||||
height: $input-height;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
width: 42px;
|
||||
background-color: $input-label-bg;
|
||||
|
@ -0,0 +1,77 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { SingleStatValueInfo, VizOrientation } from '../../types';
|
||||
|
||||
interface RenderProps {
|
||||
vizWidth: number;
|
||||
vizHeight: number;
|
||||
valueInfo: SingleStatValueInfo;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: (renderProps: RenderProps) => JSX.Element | JSX.Element[];
|
||||
height: number;
|
||||
width: number;
|
||||
values: SingleStatValueInfo[];
|
||||
orientation: VizOrientation;
|
||||
}
|
||||
|
||||
const SPACE_BETWEEN = 10;
|
||||
|
||||
export class VizRepeater extends PureComponent<Props> {
|
||||
getOrientation(): VizOrientation {
|
||||
const { orientation, width, height } = this.props;
|
||||
|
||||
if (orientation === VizOrientation.Auto) {
|
||||
if (width > height) {
|
||||
return VizOrientation.Vertical;
|
||||
} else {
|
||||
return VizOrientation.Horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, height, values, width } = this.props;
|
||||
const orientation = this.getOrientation();
|
||||
|
||||
const itemStyles: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const repeaterStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
let vizHeight = height;
|
||||
let vizWidth = width;
|
||||
|
||||
if (orientation === VizOrientation.Horizontal) {
|
||||
repeaterStyle.flexDirection = 'column';
|
||||
itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
|
||||
vizWidth = width;
|
||||
vizHeight = height / values.length - SPACE_BETWEEN;
|
||||
} else {
|
||||
repeaterStyle.flexDirection = 'row';
|
||||
itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
|
||||
vizHeight = height;
|
||||
vizWidth = width / values.length - SPACE_BETWEEN;
|
||||
}
|
||||
|
||||
itemStyles.width = `${vizWidth}px`;
|
||||
itemStyles.height = `${vizHeight}px`;
|
||||
|
||||
return (
|
||||
<div style={repeaterStyle}>
|
||||
{values.map((valueInfo, index) => {
|
||||
return (
|
||||
<div key={index} style={itemStyles}>
|
||||
{children({ vizHeight, vizWidth, valueInfo })}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Table/Table';
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
@ -10,3 +11,4 @@
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import 'EmptySearchResult/EmptySearchResult';
|
||||
@import 'FormField/FormField';
|
||||
@import 'BarGauge/BarGauge';
|
||||
|
@ -19,11 +19,15 @@ export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Switch } from './Switch/Switch';
|
||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||
|
||||
// Visualizations
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { BarGauge } from './BarGauge/BarGauge';
|
||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||
|
@ -17,7 +17,13 @@ $enable-hover-media-query: false !default;
|
||||
// Control the default styling of most Bootstrap elements by modifying these
|
||||
// variables. Mostly focused on spacing.
|
||||
|
||||
$spacer: ${theme.spacing.m} !default;
|
||||
$space-xxs: ${theme.spacing.xxs} !default;
|
||||
$space-xs: ${theme.spacing.xs} !default;
|
||||
$space-sm: ${theme.spacing.sm} !default;
|
||||
$space-md: ${theme.spacing.md} !default;
|
||||
$space-lg: ${theme.spacing.lg} !default;
|
||||
$space-xl: ${theme.spacing.xl} !default;
|
||||
$spacer: ${theme.spacing.d} !default;
|
||||
$spacer-x: $spacer !default;
|
||||
$spacer-y: $spacer !default;
|
||||
$spacers: (
|
||||
@ -46,7 +52,6 @@ $spacers: (
|
||||
),
|
||||
),
|
||||
) !default;
|
||||
$border-width: ${theme.border.width.s} !default;
|
||||
|
||||
// Grid breakpoints
|
||||
//
|
||||
@ -55,9 +60,9 @@ $border-width: ${theme.border.width.s} !default;
|
||||
|
||||
$grid-breakpoints: (
|
||||
xs: ${theme.breakpoints.xs},
|
||||
sm: ${theme.breakpoints.s},
|
||||
md: ${theme.breakpoints.m},
|
||||
lg: ${theme.breakpoints.l},
|
||||
sm: ${theme.breakpoints.sm},
|
||||
md: ${theme.breakpoints.md},
|
||||
lg: ${theme.breakpoints.lg},
|
||||
xl: ${theme.breakpoints.xl},
|
||||
) !default;
|
||||
|
||||
@ -77,27 +82,26 @@ $container-max-widths: (
|
||||
// Set the number of columns and specify the width of the gutters.
|
||||
|
||||
$grid-columns: 12 !default;
|
||||
$grid-gutter-width: 30px !default;
|
||||
|
||||
$enable-flex: true;
|
||||
$grid-gutter-width: ${theme.spacing.gutter} !default;
|
||||
|
||||
// Typography
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
|
||||
$font-family-monospace: ${theme.typography.fontFamily.monospace};
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
$font-size-root: ${theme.typography.size.root} !default;
|
||||
$font-size-base: ${theme.typography.size.base} !default;
|
||||
|
||||
$font-size-lg: ${theme.typography.size.l} !default;
|
||||
$font-size-md: ${theme.typography.size.m} !default;
|
||||
$font-size-sm: ${theme.typography.size.s} !default;
|
||||
$font-size-lg: ${theme.typography.size.lg} !default;
|
||||
$font-size-md: ${theme.typography.size.md} !default;
|
||||
$font-size-sm: ${theme.typography.size.sm} !default;
|
||||
$font-size-xs: ${theme.typography.size.xs} !default;
|
||||
|
||||
$line-height-base: ${theme.typography.lineHeight.l} !default;
|
||||
$font-weight-semi-bold: ${theme.typography.weight.semibold};
|
||||
$line-height-base: ${theme.typography.lineHeight.lg} !default;
|
||||
|
||||
$font-weight-regular: ${theme.typography.weight.regular} !default;
|
||||
$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
|
||||
|
||||
$font-size-h1: ${theme.typography.heading.h1} !default;
|
||||
$font-size-h2: ${theme.typography.heading.h2} !default;
|
||||
@ -106,24 +110,18 @@ $font-size-h4: ${theme.typography.heading.h4} !default;
|
||||
$font-size-h5: ${theme.typography.heading.h5} !default;
|
||||
$font-size-h6: ${theme.typography.heading.h6} !default;
|
||||
|
||||
$headings-margin-bottom: ($spacer / 2) !default;
|
||||
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$headings-font-weight: ${theme.typography.weight.normal} !default;
|
||||
$headings-line-height: ${theme.typography.lineHeight.s} !default;
|
||||
|
||||
$hr-border-width: $border-width !default;
|
||||
$dt-font-weight: bold !default;
|
||||
$headings-line-height: ${theme.typography.lineHeight.sm} !default;
|
||||
|
||||
// Components
|
||||
//
|
||||
// Define common padding and border radius sizes and more.
|
||||
|
||||
$line-height-lg: (4 / 3) !default;
|
||||
$line-height-sm: 1.5 !default;
|
||||
$border-width: ${theme.border.width.sm} !default;
|
||||
|
||||
$border-radius: 3px !default;
|
||||
$border-radius-lg: 5px !default;
|
||||
$border-radius-sm: 2px !default;
|
||||
$border-radius: ${theme.border.radius.md} !default;
|
||||
$border-radius-lg: ${theme.border.radius.lg}!default;
|
||||
$border-radius-sm: ${theme.border.radius.sm} !default;
|
||||
|
||||
// Page
|
||||
|
||||
@ -146,23 +144,17 @@ $input-padding-x: 10px !default;
|
||||
$input-padding-y: 8px !default;
|
||||
$input-line-height: 18px !default;
|
||||
|
||||
$input-btn-border-width: 1px;
|
||||
$input-border-radius: 0 $border-radius $border-radius 0 !default;
|
||||
$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
|
||||
|
||||
$label-border-radius: $border-radius 0 0 $border-radius !default;
|
||||
$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
|
||||
|
||||
$input-padding-y-sm: 4px !default;
|
||||
|
||||
$input-padding-x-lg: 20px !default;
|
||||
$input-padding-y-lg: 10px !default;
|
||||
|
||||
$input-height: 35px !default;
|
||||
|
||||
$gf-form-margin: 3px;
|
||||
$gf-form-input-height: 35px;
|
||||
|
||||
$cursor-disabled: not-allowed !default;
|
||||
|
||||
// Form validation icons
|
||||
@ -199,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
|
||||
$btn-padding-x-xl: 21px !default;
|
||||
$btn-padding-y-xl: 11px !default;
|
||||
|
||||
$btn-border-radius: 2px;
|
||||
|
||||
$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
|
||||
@ -207,8 +198,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
$side-menu-width: 60px;
|
||||
|
||||
// dashboard
|
||||
$panel-margin: 10px;
|
||||
$dashboard-padding: $panel-margin * 2;
|
||||
$dashboard-padding: 10px * 2;
|
||||
$panel-horizontal-padding: 10;
|
||||
$panel-vertical-padding: 5;
|
||||
$panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
|
||||
@ -243,9 +233,4 @@ $external-services: (
|
||||
icon: '',
|
||||
),
|
||||
) !default;
|
||||
|
||||
:export {
|
||||
panelhorizontalpadding: $panel-horizontal-padding;
|
||||
panelverticalpadding: $panel-vertical-padding;
|
||||
}
|
||||
`;
|
||||
|
@ -11,9 +11,9 @@ const theme: GrafanaThemeCommons = {
|
||||
root: '14px',
|
||||
base: '13px',
|
||||
xs: '10px',
|
||||
s: '12px',
|
||||
m: '14px',
|
||||
l: '18px',
|
||||
sm: '12px',
|
||||
md: '14px',
|
||||
lg: '18px',
|
||||
},
|
||||
heading: {
|
||||
h1: '28px',
|
||||
@ -25,40 +25,47 @@ const theme: GrafanaThemeCommons = {
|
||||
},
|
||||
weight: {
|
||||
light: 300,
|
||||
normal: 400,
|
||||
regular: 400,
|
||||
semibold: 500,
|
||||
},
|
||||
lineHeight: {
|
||||
xs: 1,
|
||||
s: 1.1,
|
||||
m: 4 / 3,
|
||||
l: 1.5,
|
||||
sm: 1.1,
|
||||
md: 4 / 3,
|
||||
lg: 1.5,
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
xs: '0',
|
||||
s: '544px',
|
||||
m: '768px',
|
||||
l: '992px',
|
||||
sm: '544px',
|
||||
md: '768px',
|
||||
lg: '992px',
|
||||
xl: '1200px',
|
||||
},
|
||||
spacing: {
|
||||
xs: '0',
|
||||
s: '3px',
|
||||
m: '14px',
|
||||
l: '21px',
|
||||
d: '14px',
|
||||
xxs: '2px',
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
gutter: '30px',
|
||||
},
|
||||
border: {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
s: '3px',
|
||||
m: '5px',
|
||||
sm: '2px',
|
||||
md: '3px',
|
||||
lg: '5px',
|
||||
},
|
||||
width: {
|
||||
s: '1px',
|
||||
sm: '1px',
|
||||
},
|
||||
},
|
||||
panelPadding: {
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
@ -48,10 +48,7 @@ export enum NullValueMode {
|
||||
}
|
||||
|
||||
/** View model projection of many time series */
|
||||
export interface TimeSeriesVMs {
|
||||
[index: number]: TimeSeriesVM;
|
||||
length: number;
|
||||
}
|
||||
export type TimeSeriesVMs = TimeSeriesVM[];
|
||||
|
||||
export interface Column {
|
||||
text: string; // name
|
||||
@ -64,3 +61,12 @@ export interface TableData {
|
||||
columns: Column[];
|
||||
rows: any[];
|
||||
}
|
||||
|
||||
export type SingleStatValue = number | string | null;
|
||||
|
||||
/*
|
||||
* So we can add meta info like tags & series name
|
||||
*/
|
||||
export interface SingleStatValueInfo {
|
||||
value: SingleStatValue;
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ import { PluginMeta } from './plugin';
|
||||
import { TableData, TimeSeries } from './data';
|
||||
|
||||
export interface DataQueryResponse {
|
||||
data: TimeSeries[] | [TableData] | any;
|
||||
data: DataQueryResponseData;
|
||||
}
|
||||
|
||||
export type DataQueryResponseData = TimeSeries[] | [TableData] | any;
|
||||
|
||||
export interface DataQuery {
|
||||
/**
|
||||
* A - Z
|
||||
|
@ -4,3 +4,4 @@ export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './datasource';
|
||||
export * from './theme';
|
||||
export * from './threshold';
|
||||
|
@ -21,10 +21,13 @@ export interface PanelEditorProps<T = any> {
|
||||
onOptionsChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
preserveOptions?: PreservePanelOptionsHandler<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
@ -37,6 +40,10 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
setDefaults(defaults: TOptions) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
|
||||
this.preserveOptions = handler;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelSize {
|
||||
@ -53,17 +60,6 @@ export interface PanelMenuItem {
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export enum BasicGaugeColor {
|
||||
Green = '#299c46',
|
||||
Red = '#d44a3a',
|
||||
}
|
||||
|
||||
export enum MappingType {
|
||||
ValueToText = 1,
|
||||
RangeToText = 2,
|
||||
@ -86,3 +82,9 @@ export interface RangeMap extends BaseMap {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export enum VizOrientation {
|
||||
Auto = 'auto',
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ export interface GrafanaThemeCommons {
|
||||
// TODO: not sure if should be a part of theme
|
||||
breakpoints: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
};
|
||||
typography: {
|
||||
@ -22,20 +22,20 @@ export interface GrafanaThemeCommons {
|
||||
root: string;
|
||||
base: string;
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
};
|
||||
weight: {
|
||||
light: number;
|
||||
normal: number;
|
||||
regular: number;
|
||||
semibold: number;
|
||||
};
|
||||
lineHeight: {
|
||||
xs: number; //1
|
||||
s: number; //1.1
|
||||
m: number; // 4/3
|
||||
l: number; // 1.5
|
||||
sm: number; //1.1
|
||||
md: number; // 4/3
|
||||
lg: number; // 1.5
|
||||
};
|
||||
// TODO: Refactor to use size instead of custom defs
|
||||
heading: {
|
||||
@ -48,22 +48,29 @@ export interface GrafanaThemeCommons {
|
||||
};
|
||||
};
|
||||
spacing: {
|
||||
d: string;
|
||||
xxs: string;
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
gutter: string;
|
||||
};
|
||||
border: {
|
||||
radius: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
};
|
||||
width: {
|
||||
s: string;
|
||||
sm: string;
|
||||
};
|
||||
};
|
||||
panelPadding: {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GrafanaTheme extends GrafanaThemeCommons {
|
||||
|
5
packages/grafana-ui/src/types/threshold.ts
Normal file
5
packages/grafana-ui/src/types/threshold.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './processTableData';
|
||||
export * from './singlestat';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './thresholds';
|
||||
export * from './string';
|
||||
export * from './deprecationWarning';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { TableData, Column, TimeSeries } from '../types/index';
|
||||
|
||||
// Libraries
|
||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
|
||||
// Types
|
||||
import { TableData, Column, TimeSeries } from '../types';
|
||||
|
||||
// Subset of all parse options
|
||||
export interface TableParseOptions {
|
||||
@ -163,3 +166,24 @@ export const toTableData = (results?: any[]): TableData[] => {
|
||||
throw new Error('Unsupported data format');
|
||||
});
|
||||
};
|
||||
|
||||
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
|
||||
if (isNumber(sortIndex)) {
|
||||
const copy = {
|
||||
...data,
|
||||
rows: [...data.rows].sort((a, b) => {
|
||||
a = a[sortIndex];
|
||||
b = b[sortIndex];
|
||||
// Sort null or undefined separately from comparable values
|
||||
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
|
||||
}),
|
||||
};
|
||||
|
||||
if (reverse) {
|
||||
copy.rows.reverse();
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
31
packages/grafana-ui/src/utils/singlestat.ts
Normal file
31
packages/grafana-ui/src/utils/singlestat.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { TableData, NullValueMode, SingleStatValueInfo } from '../types';
|
||||
import { processTimeSeries } from './processTimeSeries';
|
||||
|
||||
export interface SingleStatProcessingOptions {
|
||||
data: TableData[];
|
||||
stat: string;
|
||||
}
|
||||
|
||||
//
|
||||
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
|
||||
//
|
||||
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
|
||||
const { data, stat } = options;
|
||||
|
||||
const timeSeries = processTimeSeries({
|
||||
data,
|
||||
xColumn: 0,
|
||||
yColumn: 0,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
return timeSeries.map((series, index) => {
|
||||
const value = stat !== 'name' ? series.stats[stat] : series.label;
|
||||
|
||||
return {
|
||||
value: value,
|
||||
};
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
/** This will add full size with & height properties */
|
||||
export const withFullSizeStory = (component: React.ComponentType<any>, props: any) => (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<>
|
||||
{React.createElement(component, {
|
||||
...props,
|
||||
width,
|
||||
height,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
23
packages/grafana-ui/src/utils/thresholds.ts
Normal file
23
packages/grafana-ui/src/utils/thresholds.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Threshold } from '../types';
|
||||
|
||||
export function getThresholdForValue(
|
||||
thresholds: Threshold[],
|
||||
value: number | null | string | undefined
|
||||
): Threshold | null {
|
||||
if (thresholds.length === 1) {
|
||||
return thresholds[0];
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return atThreshold;
|
||||
}
|
||||
|
||||
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
||||
if (belowThreshold.length > 0) {
|
||||
const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
|
||||
return nearestThreshold;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -192,16 +192,18 @@ func getPanelSort(id string) int {
|
||||
sort = 2
|
||||
case "gauge":
|
||||
sort = 3
|
||||
case "table":
|
||||
case "bargauge":
|
||||
sort = 4
|
||||
case "text":
|
||||
case "table":
|
||||
sort = 5
|
||||
case "heatmap":
|
||||
case "text":
|
||||
sort = 6
|
||||
case "alertlist":
|
||||
case "heatmap":
|
||||
sort = 7
|
||||
case "dashlist":
|
||||
case "alertlist":
|
||||
sort = 8
|
||||
case "dashlist":
|
||||
sort = 9
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
|
||||
}
|
||||
|
||||
for _, query := range tsdbQuery.Queries {
|
||||
glog.Info("graphite", "query", query.Model)
|
||||
glog.Debug("graphite", "query", query.Model)
|
||||
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
|
||||
target = fixIntervalFormat(fullTarget)
|
||||
} else {
|
||||
|
@ -10,13 +10,13 @@ coreModule.directive('jsonTree', [
|
||||
startExpanded: '@',
|
||||
rootName: '@',
|
||||
},
|
||||
link: (scope, elem) => {
|
||||
link: (scope: any, elem) => {
|
||||
const jsonExp = new JsonExplorer(scope.object, 3, {
|
||||
animateOpen: true,
|
||||
});
|
||||
|
||||
const html = jsonExp.render(true);
|
||||
elem.html(html);
|
||||
elem.replaceAll(html);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -14,7 +14,7 @@ coreModule.directive('giveFocus', () => {
|
||||
}
|
||||
setTimeout(() => {
|
||||
element.focus();
|
||||
const domEl = element[0];
|
||||
const domEl: any = element[0];
|
||||
if (domEl.setSelectionRange) {
|
||||
const pos = element.val().length * 2;
|
||||
domEl.setSelectionRange(pos, pos);
|
||||
|
@ -1,15 +0,0 @@
|
||||
import kbn from '../utils/kbn';
|
||||
|
||||
describe('stringToJsRegex', () => {
|
||||
it('should parse the valid regex value', () => {
|
||||
const output = kbn.stringToJsRegex('/validRegexp/');
|
||||
expect(output).toBeInstanceOf(RegExp);
|
||||
});
|
||||
|
||||
it('should throw error on invalid regex value', () => {
|
||||
const input = '/etc/hostname';
|
||||
expect(() => {
|
||||
kbn.stringToJsRegex(input);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
@ -15,6 +15,7 @@ export const provideConfig = (component: React.ComponentType<any>) => {
|
||||
|
||||
export const getCurrentThemeName = () =>
|
||||
config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
|
||||
|
||||
export const getCurrentTheme = () => getTheme(getCurrentThemeName());
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ResultType,
|
||||
QueryIntervals,
|
||||
QueryOptions,
|
||||
ResultGetter,
|
||||
} from 'app/types/explore';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
|
||||
@ -301,11 +302,24 @@ export function getIntervals(range: RawTimeRange, lowLimit: string, resolution:
|
||||
return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
|
||||
}
|
||||
|
||||
export function makeTimeSeriesList(dataList) {
|
||||
return dataList.map((seriesData, index) => {
|
||||
export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
|
||||
// Prevent multiple Graph transactions to have the same colors
|
||||
let colorIndexOffset = 0;
|
||||
for (const other of allTransactions) {
|
||||
// Only need to consider transactions that came before the current one
|
||||
if (other === transaction) {
|
||||
break;
|
||||
}
|
||||
// Count timeseries of previous query results
|
||||
if (other.resultType === 'Graph' && other.done) {
|
||||
colorIndexOffset += other.result.length;
|
||||
}
|
||||
}
|
||||
|
||||
return dataList.map((seriesData, index: number) => {
|
||||
const datapoints = seriesData.datapoints || [];
|
||||
const alias = seriesData.target;
|
||||
const colorIndex = index % colors.length;
|
||||
const colorIndex = (colorIndexOffset + index) % colors.length;
|
||||
const color = colors[colorIndex];
|
||||
|
||||
const series = new TimeSeries({
|
||||
@ -317,7 +331,7 @@ export function makeTimeSeriesList(dataList) {
|
||||
|
||||
return series;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the query history. Side-effect: store history in local storage
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
.gicon {
|
||||
font-size: 30px;
|
||||
margin-right: $spacer;
|
||||
margin-right: $space-md;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@ -32,16 +32,16 @@
|
||||
.add-panel-widget__title {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
margin-right: $spacer * 2;
|
||||
margin-right: $space-xl;
|
||||
}
|
||||
|
||||
.add-panel-widget__link {
|
||||
margin: 0 8px;
|
||||
margin: 0 $space-sm;
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
.add-panel-widget__icon {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: $space-sm;
|
||||
|
||||
.gicon {
|
||||
color: white;
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
.add-panel-widget__create {
|
||||
display: inherit;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: $space-lg;
|
||||
// this is to have the big button appear centered
|
||||
margin-top: 55px;
|
||||
}
|
||||
@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
.add-panel-widget__action {
|
||||
margin: 0 4px;
|
||||
margin: 0 $space-xs;
|
||||
}
|
||||
|
||||
.add-panel-widget__btn-container {
|
||||
|
@ -16,9 +16,6 @@
|
||||
<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
|
||||
Save As...
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -70,6 +67,11 @@
|
||||
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
Delete Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'" ng-include="'public/app/features/annotations/partials/editor.html'">
|
||||
|
@ -6,11 +6,6 @@ import { cleanUpDashboard } from '../state/actions';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||
|
||||
jest.mock('sass/_variables.generated.scss', () => ({
|
||||
panelhorizontalpadding: 10,
|
||||
panelVerticalPadding: 10,
|
||||
}));
|
||||
|
||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||
|
||||
interface ScenarioContext {
|
||||
|
@ -92,11 +92,12 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
componentWillUnmount() {
|
||||
if (this.props.dashboard) {
|
||||
this.props.cleanUpDashboard();
|
||||
this.setPanelFullscreenClass(false);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props;
|
||||
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId, urlUid } = this.props;
|
||||
|
||||
if (!dashboard) {
|
||||
return;
|
||||
@ -107,6 +108,12 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
document.title = dashboard.title + ' - Grafana';
|
||||
}
|
||||
|
||||
// Due to the angular -> react url bridge we can ge an update here with new uid before the container unmounts
|
||||
// Can remove this condition after we switch to react router
|
||||
if (prevProps.urlUid !== urlUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle animation states when opening dashboard settings
|
||||
if (!prevProps.editview && editview) {
|
||||
this.setState({ isSettingsOpening: true });
|
||||
@ -163,14 +170,20 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
fullscreenPanel: null,
|
||||
scrollTop: this.state.rememberScrollTop,
|
||||
},
|
||||
() => {
|
||||
dashboard.render();
|
||||
}
|
||||
this.triggerPanelsRendering.bind(this)
|
||||
);
|
||||
|
||||
this.setPanelFullscreenClass(false);
|
||||
}
|
||||
|
||||
triggerPanelsRendering() {
|
||||
try {
|
||||
this.props.dashboard.render();
|
||||
} catch (err) {
|
||||
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
|
||||
}
|
||||
}
|
||||
|
||||
handleFullscreenPanelNotFound(urlPanelId: string) {
|
||||
// Panel not found
|
||||
this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
|
||||
|
@ -76,21 +76,26 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
// unmount angular panel
|
||||
this.cleanUpAngularPanel();
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
this.props.panel.changeType(pluginId, fromAngularPanel);
|
||||
}
|
||||
|
||||
if (plugin.exports) {
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
} else {
|
||||
if (!plugin.exports) {
|
||||
try {
|
||||
plugin.exports = await importPluginModule(plugin.module);
|
||||
} catch (e) {
|
||||
plugin = getPanelPluginNotFound(pluginId);
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
if (fromAngularPanel) {
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
panel.destroy();
|
||||
|
||||
this.props.panel.changeType(pluginId);
|
||||
} else {
|
||||
panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ export class DataPanel extends Component<Props, State> {
|
||||
this.setState({ loading: LoadingState.Loading });
|
||||
|
||||
try {
|
||||
const ds = await this.dataSourceSrv.get(datasource);
|
||||
const ds = await this.dataSourceSrv.get(datasource, scopedVars);
|
||||
|
||||
// TODO interpolate variables
|
||||
const minInterval = this.props.minInterval || ds.interval;
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
|
||||
jest.mock('sass/_variables.generated.scss', () => ({
|
||||
panelhorizontalpadding: 10,
|
||||
panelVerticalPadding: 10,
|
||||
}));
|
||||
|
||||
describe('PanelChrome', () => {
|
||||
let chrome: PanelChrome;
|
||||
|
||||
|
@ -14,6 +14,7 @@ import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import config from 'app/core/config';
|
||||
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
@ -21,7 +22,6 @@ import { PanelPlugin } from 'app/types';
|
||||
import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
|
||||
import { ScopedVars } from '@grafana/ui';
|
||||
|
||||
import variables from 'sass/_variables.generated.scss';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
@ -160,8 +160,8 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
data={data}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
|
||||
width={width - 2 * variables.panelhorizontalpadding}
|
||||
height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
|
||||
width={width - 2 * config.theme.panelPadding.horizontal}
|
||||
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={this.replaceVariables}
|
||||
/>
|
||||
|
@ -2,9 +2,12 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
|
||||
|
||||
// Types
|
||||
import { PanelPlugin, AppNotificationSeverity } from 'app/types';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
@ -19,15 +22,13 @@ class PanelPluginNotFound extends PureComponent<Props> {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center' as 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div className="alert alert-error" style={{ margin: '0 auto' }}>
|
||||
Panel plugin with id {this.props.pluginId} could not be found
|
||||
</div>
|
||||
<AlertBox severity={AppNotificationSeverity.Error} title={`Panel plugin not found: ${this.props.pluginId}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { PanelModel } from './PanelModel';
|
||||
|
||||
describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
@ -66,7 +65,7 @@ describe('PanelModel', () => {
|
||||
|
||||
describe('when changing panel type', () => {
|
||||
beforeEach(() => {
|
||||
model.changeType('graph', true);
|
||||
model.changeType('graph');
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
@ -75,12 +74,12 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
it('should restore table properties when changing back', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.showColumns).toBe(true);
|
||||
});
|
||||
|
||||
it('should remove alert rule when changing type that does not support it', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.alert).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
@ -229,10 +229,6 @@ export class PanelModel {
|
||||
}, {});
|
||||
}
|
||||
|
||||
private saveCurrentPanelOptions() {
|
||||
this.cachedPluginOptions[this.type] = this.getOptionsToRemember();
|
||||
}
|
||||
|
||||
private restorePanelOptions(pluginId: string) {
|
||||
const prevOptions = this.cachedPluginOptions[pluginId] || {};
|
||||
|
||||
@ -241,14 +237,11 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
changeType(pluginId: string, fromAngularPanel: boolean) {
|
||||
this.saveCurrentPanelOptions();
|
||||
this.type = pluginId;
|
||||
changeType(pluginId: string, preserveOptions?: any) {
|
||||
const oldOptions: any = this.getOptionsToRemember();
|
||||
const oldPluginId = this.type;
|
||||
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
if (fromAngularPanel) {
|
||||
this.destroy();
|
||||
}
|
||||
this.type = pluginId;
|
||||
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
@ -259,7 +252,13 @@ export class PanelModel {
|
||||
delete this[key];
|
||||
}
|
||||
|
||||
this.cachedPluginOptions[oldPluginId] = oldOptions;
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
||||
if (preserveOptions && oldOptions) {
|
||||
this.options = this.options || {};
|
||||
Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
|
||||
}
|
||||
}
|
||||
|
||||
addQuery(query?: Partial<DataQuery>) {
|
||||
|
@ -9,7 +9,7 @@ coreModule.directive('datasourceHttpSettings', () => {
|
||||
},
|
||||
templateUrl: 'public/app/features/datasources/partials/http_settings.html',
|
||||
link: {
|
||||
pre: ($scope, elem, attrs) => {
|
||||
pre: ($scope: any, elem, attrs) => {
|
||||
// do not show access option if direct access is disabled
|
||||
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
|
||||
$scope.showAccessHelp = false;
|
||||
|
@ -597,7 +597,8 @@ function runQueriesForType(
|
||||
const res = await datasourceInstance.query(transaction.options);
|
||||
eventBridge.emit('data-received', res.data || []);
|
||||
const latency = Date.now() - now;
|
||||
const results = resultGetter ? resultGetter(res.data) : res.data;
|
||||
const { queryTransactions } = getState().explore[exploreId];
|
||||
const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
|
||||
dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
|
||||
} catch (response) {
|
||||
eventBridge.emit('data-error', response);
|
||||
|
@ -81,7 +81,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
|
||||
// load datasource service
|
||||
this.datasourceSrv
|
||||
.get(this.panel.datasource)
|
||||
.get(this.panel.datasource, this.panel.scopedVars)
|
||||
.then(this.updateTimeRange.bind(this))
|
||||
.then(this.issueQueries.bind(this))
|
||||
.then(this.handleQueryResult.bind(this))
|
||||
|
@ -33,7 +33,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
template: panelTemplate,
|
||||
transclude: true,
|
||||
scope: { ctrl: '=' },
|
||||
link: (scope, elem) => {
|
||||
link: (scope: any, elem) => {
|
||||
const panelContainer = elem.find('.panel-container');
|
||||
const panelContent = elem.find('.panel-content');
|
||||
const cornerInfoElem = elem.find('.panel-info-corner');
|
||||
@ -67,7 +67,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
// set initial transparency
|
||||
if (ctrl.panel.transparent) {
|
||||
transparentLastState = true;
|
||||
panelContainer.addClass('panel-transparent', true);
|
||||
panelContainer.addClass('panel-transparent');
|
||||
}
|
||||
|
||||
// update scrollbar after mounting
|
||||
|
@ -23,9 +23,11 @@ import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||
import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as table2Panel from 'app/plugins/panel/table2/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
|
||||
const builtInPlugins = {
|
||||
'app/plugins/datasource/graphite/module': graphitePlugin,
|
||||
@ -53,9 +55,11 @@ const builtInPlugins = {
|
||||
'app/plugins/panel/alertlist/module': alertListPanel,
|
||||
'app/plugins/panel/heatmap/module': heatmapPanel,
|
||||
'app/plugins/panel/table/module': tablePanel,
|
||||
'app/plugins/panel/table2/module': table2Panel,
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
'app/plugins/panel/bargauge/module': barGaugePanel,
|
||||
};
|
||||
|
||||
export default builtInPlugins;
|
||||
|
@ -7,7 +7,7 @@ import config from 'app/core/config';
|
||||
import { importPluginModule } from './plugin_loader';
|
||||
|
||||
// Types
|
||||
import { DataSourceApi, DataSourceSelectItem } from '@grafana/ui/src/types';
|
||||
import { DataSourceApi, DataSourceSelectItem, ScopedVars } from '@grafana/ui/src/types';
|
||||
|
||||
export class DatasourceSrv {
|
||||
datasources: { [name: string]: DataSourceApi };
|
||||
@ -21,12 +21,18 @@ export class DatasourceSrv {
|
||||
this.datasources = {};
|
||||
}
|
||||
|
||||
get(name?: string): Promise<DataSourceApi> {
|
||||
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
if (!name) {
|
||||
return this.get(config.defaultDatasource);
|
||||
}
|
||||
|
||||
name = this.templateSrv.replace(name);
|
||||
// Interpolation here is to support template variable in data source selection
|
||||
name = this.templateSrv.replace(name, scopedVars, (value, variable) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
if (name === 'default') {
|
||||
return this.get(config.defaultDatasource);
|
||||
|
@ -6,6 +6,8 @@ export class DatasourceVariable implements Variable {
|
||||
query: string;
|
||||
options: any;
|
||||
current: any;
|
||||
multi: boolean;
|
||||
includeAll: boolean;
|
||||
refresh: any;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
@ -18,6 +20,8 @@ export class DatasourceVariable implements Variable {
|
||||
regex: '',
|
||||
options: [],
|
||||
query: '',
|
||||
multi: false,
|
||||
includeAll: false,
|
||||
refresh: 1,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
@ -69,9 +73,16 @@ export class DatasourceVariable implements Variable {
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
if (this.includeAll) {
|
||||
this.addAllOption();
|
||||
}
|
||||
return this.variableSrv.validateVariableSelectionState(this);
|
||||
}
|
||||
|
||||
addAllOption() {
|
||||
this.options.unshift({ text: 'All', value: '$__all' });
|
||||
}
|
||||
|
||||
dependsOn(variable) {
|
||||
if (this.regex) {
|
||||
return containsVariable(this.regex, variable.name);
|
||||
@ -84,6 +95,9 @@ export class DatasourceVariable implements Variable {
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
if (this.current.text === 'All') {
|
||||
return 'All';
|
||||
}
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
@ -91,5 +105,6 @@ export class DatasourceVariable implements Variable {
|
||||
variableTypes['datasource'] = {
|
||||
name: 'Datasource',
|
||||
ctor: DatasourceVariable,
|
||||
supportsMulti: true,
|
||||
description: 'Enabled you to dynamically switch the datasource for multiple panels',
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { TimeSeries } from '@grafana/ui';
|
||||
|
||||
export class ResultTransformer {
|
||||
constructor(private templateSrv) {}
|
||||
@ -18,10 +19,10 @@ export class ResultTransformer {
|
||||
];
|
||||
} else if (prometheusResult && options.format === 'heatmap') {
|
||||
let seriesList = [];
|
||||
prometheusResult.sort(sortSeriesByLabel);
|
||||
for (const metricData of prometheusResult) {
|
||||
seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
|
||||
}
|
||||
seriesList.sort(sortSeriesByLabel);
|
||||
seriesList = this.transformToHistogramOverTime(seriesList);
|
||||
return seriesList;
|
||||
} else if (prometheusResult) {
|
||||
@ -197,13 +198,13 @@ export class ResultTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
function sortSeriesByLabel(s1, s2): number {
|
||||
function sortSeriesByLabel(s1: TimeSeries, s2: TimeSeries): number {
|
||||
let le1, le2;
|
||||
|
||||
try {
|
||||
// fail if not integer. might happen with bad queries
|
||||
le1 = parseHistogramLabel(s1.metric.le);
|
||||
le2 = parseHistogramLabel(s2.metric.le);
|
||||
le1 = parseHistogramLabel(s1.target);
|
||||
le2 = parseHistogramLabel(s2.target);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return 0;
|
||||
|
@ -2,28 +2,6 @@ import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
import { FilterSegments, DefaultFilterValue } from './filter_segments';
|
||||
|
||||
export class StackdriverFilter {
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
return {
|
||||
templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
|
||||
controller: 'StackdriverFilterCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
labelData: '<',
|
||||
loading: '<',
|
||||
groupBys: '<',
|
||||
filters: '<',
|
||||
filtersChanged: '&',
|
||||
groupBysChanged: '&',
|
||||
hideGroupBys: '<',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class StackdriverFilterCtrl {
|
||||
defaultRemoveGroupByValue = '-- remove group by --';
|
||||
resourceTypeValue = 'resource.type';
|
||||
@ -193,5 +171,24 @@ export class StackdriverFilterCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('stackdriverFilter', StackdriverFilter);
|
||||
coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
|
||||
/** @ngInject */
|
||||
function stackdriverFilter() {
|
||||
return {
|
||||
templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
|
||||
controller: StackdriverFilterCtrl,
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
labelData: '<',
|
||||
loading: '<',
|
||||
groupBys: '<',
|
||||
filters: '<',
|
||||
filtersChanged: '&',
|
||||
groupBysChanged: '&',
|
||||
hideGroupBys: '<',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('stackdriverFilter', stackdriverFilter);
|
||||
|
56
public/app/plugins/panel/bargauge/BarGaugePanel.tsx
Normal file
56
public/app/plugins/panel/bargauge/BarGaugePanel.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { BarGauge, VizRepeater } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { BarGaugeOptions } from './types';
|
||||
import { PanelProps } from '@grafana/ui/src/types';
|
||||
|
||||
interface Props extends PanelProps<BarGaugeOptions> {}
|
||||
|
||||
export class BarGaugePanel extends PureComponent<Props> {
|
||||
renderBarGauge(value, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
|
||||
return (
|
||||
<BarGauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
orientation={options.orientation}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, options, width, height } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
data,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={options.orientation}>
|
||||
{({ vizHeight, vizWidth, valueInfo }) => this.renderBarGauge(valueInfo.value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
);
|
||||
}
|
||||
}
|
64
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
Normal file
64
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
||||
import { BarGaugeOptions, orientationOptions } from './types';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueOptions,
|
||||
});
|
||||
|
||||
onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
|
||||
onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
|
||||
onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
|
||||
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||
<PanelOptionsGroup title="Gauge">
|
||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
|
||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
|
||||
<div className="form-field">
|
||||
<FormLabel width={8}>Orientation</FormLabel>
|
||||
<Select
|
||||
width={12}
|
||||
options={orientationOptions}
|
||||
defaultValue={orientationOptions[0]}
|
||||
onChange={this.onOrientationChange}
|
||||
value={orientationOptions.find(item => item.value === options.orientation)}
|
||||
/>
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
96
public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg
Normal file
96
public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg
Normal file
@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="218.21px" height="187.4px" viewBox="0 0 218.21 187.4" style="enable-background:new 0 0 218.21 187.4;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
.st7{fill:url(#SVGID_8_);}
|
||||
.st8{fill:url(#SVGID_9_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="68.7753" y1="8.4663" x2="68.7753" y2="59.7143">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M7.61,50.71V12.52c0-2.66,1.95-4.82,4.36-4.82h113.62c2.41,0,4.36,2.16,4.36,4.82v38.19
|
||||
c0,2.66-1.95,4.82-4.36,4.82H11.97C9.56,55.53,7.61,53.37,7.61,50.71z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="152.8987" y1="19.9183" x2="152.3315" y2="39.7688">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M153.89,28.04c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
|
||||
c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
|
||||
c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.98-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
|
||||
c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
|
||||
c0.1-0.06,0.27-0.11,0.5-0.15C153.5,28.06,153.71,28.04,153.89,28.04z M154.83,35.02c0-1.05-0.21-1.91-0.62-2.57
|
||||
c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55c0,1.03,0.22,1.88,0.67,2.55
|
||||
c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C154.62,36.9,154.83,36.05,154.83,35.02z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="167.1879" y1="20.3265" x2="166.6207" y2="40.1771">
|
||||
<stop offset="0" style="stop-color:#FAA91F"/>
|
||||
<stop offset="1" style="stop-color:#F37324"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M173.57,26.79c0,0.71-0.09,1.37-0.27,1.97c-0.18,0.61-0.43,1.18-0.73,1.73c-0.3,0.55-0.65,1.07-1.03,1.58
|
||||
c-0.38,0.51-0.78,0.99-1.18,1.46l-4.01,4.65h6.95v4.19h-12.72v-4.31l6.53-7.5c0.53-0.61,0.94-1.23,1.24-1.87
|
||||
c0.3-0.64,0.46-1.25,0.46-1.84c0-0.63-0.15-1.16-0.44-1.61c-0.29-0.45-0.75-0.67-1.38-0.67c-0.59,0-1.07,0.24-1.46,0.71
|
||||
c-0.38,0.48-0.62,1.17-0.7,2.08l-4.56-0.43c0.24-2.19,0.99-3.82,2.23-4.9c1.24-1.08,2.79-1.62,4.63-1.62
|
||||
c0.99,0,1.88,0.15,2.67,0.46c0.79,0.3,1.46,0.73,2.02,1.28c0.56,0.55,0.99,1.21,1.29,2C173.41,24.94,173.57,25.82,173.57,26.79z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="90.8472" y1="69.5294" x2="90.8472" y2="118.2775">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st3" d="M7.61,115.16V71.24c0-1.08,0.87-1.95,1.95-1.95h162.57c1.08,0,1.95,0.87,1.95,1.95v43.92
|
||||
c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,117.11,7.61,116.23,7.61,115.16z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="195.8516" y1="81.1631" x2="196.9859" y2="100.4465">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M196.03,103.72h-5.28l6.35-17.22h-7.38v-4.28h12.66v3.64L196.03,103.72z"/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="208.6078" y1="80.4128" x2="209.7422" y2="99.6962">
|
||||
<stop offset="0" style="stop-color:#E81E25"/>
|
||||
<stop offset="1" style="stop-color:#A91D39"/>
|
||||
</linearGradient>
|
||||
<path class="st5" d="M210.98,89.39c0.95,0,1.78,0.18,2.47,0.55c0.7,0.36,1.28,0.86,1.75,1.49c0.47,0.63,0.81,1.37,1.05,2.23
|
||||
c0.23,0.86,0.35,1.79,0.35,2.78c0,1.24-0.17,2.33-0.52,3.29c-0.34,0.96-0.83,1.77-1.46,2.43c-0.63,0.66-1.37,1.16-2.22,1.5
|
||||
c-0.85,0.34-1.78,0.52-2.79,0.52c-1.03,0-1.99-0.17-2.85-0.5c-0.87-0.33-1.62-0.83-2.25-1.49c-0.63-0.66-1.12-1.46-1.47-2.41
|
||||
c-0.35-0.95-0.53-2.03-0.53-3.25c0-1.32,0.17-2.5,0.52-3.55c0.34-1.05,0.78-2.07,1.31-3.04l4.1-7.71h5.56l-4.16,7.35l0.03,0.03
|
||||
c0.1-0.06,0.27-0.11,0.5-0.15C210.59,89.41,210.8,89.39,210.98,89.39z M211.92,96.37c0-1.05-0.21-1.91-0.62-2.57
|
||||
c-0.41-0.66-0.99-0.99-1.72-0.99c-0.69,0-1.26,0.33-1.72,1c-0.46,0.67-0.68,1.52-0.68,2.55s0.22,1.88,0.67,2.55
|
||||
c0.44,0.67,1.01,1,1.7,1c0.71,0,1.28-0.33,1.72-1C211.7,98.25,211.92,97.4,211.92,96.37z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="49.8893" y1="131.8424" x2="49.8893" y2="178.0907">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st6" d="M7.61,176.74v-43.92c0-1.08,0.87-1.95,1.95-1.95h80.66c1.08,0,1.95,0.87,1.95,1.95v43.92
|
||||
c0,1.08-0.87,1.95-1.95,1.95H9.56C8.48,178.69,7.61,177.81,7.61,176.74z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="112.038" y1="145.9514" x2="118.2768" y2="172.6079">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st7" d="M119.69,161.21v4.31h-4.31v-4.31h-7.87v-4.13l6.29-13.06h5.89v13.18h2.19v4.01H119.69z M115.47,148.77h-0.06
|
||||
l-3.68,8.44h3.74V148.77z"/>
|
||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="126.6068" y1="142.5417" x2="132.8455" y2="169.1982">
|
||||
<stop offset="0" style="stop-color:#04A64D"/>
|
||||
<stop offset="1" style="stop-color:#007E39"/>
|
||||
</linearGradient>
|
||||
<path class="st8" d="M136.52,159.24c0,1.09-0.18,2.06-0.53,2.88c-0.35,0.83-0.84,1.53-1.44,2.11c-0.61,0.58-1.32,1.01-2.14,1.31
|
||||
c-0.82,0.29-1.71,0.44-2.66,0.44c-1.76,0-3.27-0.45-4.52-1.35c-1.26-0.9-2.09-2.27-2.49-4.11l4.28-1.09
|
||||
c0.16,0.71,0.45,1.29,0.85,1.73c0.4,0.45,0.96,0.67,1.67,0.67c0.38,0,0.72-0.08,1-0.24c0.28-0.16,0.52-0.37,0.71-0.64
|
||||
s0.33-0.56,0.43-0.9c0.09-0.33,0.14-0.67,0.14-1.02c0-0.93-0.27-1.62-0.8-2.07c-0.54-0.45-1.25-0.67-2.14-0.67h-1.09v-3.58h1.21
|
||||
c0.81,0,1.43-0.24,1.87-0.71c0.43-0.48,0.65-1.14,0.65-1.99c0-0.55-0.16-1.06-0.47-1.55c-0.31-0.49-0.82-0.73-1.5-0.73
|
||||
c-0.59,0-1.04,0.19-1.37,0.58c-0.32,0.39-0.57,0.87-0.73,1.46l-4.16-1.03c0.24-0.93,0.59-1.72,1.05-2.37s0.97-1.18,1.55-1.59
|
||||
c0.58-0.42,1.21-0.72,1.9-0.91c0.69-0.19,1.39-0.29,2.1-0.29c0.79,0,1.55,0.12,2.29,0.36c0.74,0.24,1.4,0.61,1.99,1.09
|
||||
c0.59,0.49,1.06,1.09,1.41,1.82c0.35,0.73,0.53,1.58,0.53,2.55c0,1.26-0.27,2.31-0.8,3.17c-0.54,0.86-1.25,1.43-2.14,1.72v0.09
|
||||
c1.03,0.28,1.85,0.87,2.46,1.76C136.22,157.04,136.52,158.07,136.52,159.24z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.7 KiB |
22
public/app/plugins/panel/bargauge/module.tsx
Normal file
22
public/app/plugins/panel/bargauge/module.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
|
||||
|
||||
reactPanel.setEditor(BarGaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
|
||||
const options: Partial<BarGaugeOptions> = {};
|
||||
|
||||
if (prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
19
public/app/plugins/panel/bargauge/plugin.json
Normal file
19
public/app/plugins/panel/bargauge/plugin.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Bar Gauge",
|
||||
"id": "bargauge",
|
||||
"state": "alpha",
|
||||
|
||||
"dataFormats": ["time_series"],
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icon_bar_gauge.svg",
|
||||
"large": "img/icon_bar_gauge.svg"
|
||||
}
|
||||
}
|
||||
}
|
31
public/app/plugins/panel/bargauge/types.ts
Normal file
31
public/app/plugins/panel/bargauge/types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export interface BarGaugeOptions {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
orientation: VizOrientation;
|
||||
valueOptions: SingleStatValueOptions;
|
||||
valueMappings: ValueMapping[];
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
|
||||
export const orientationOptions: SelectOptionItem[] = [
|
||||
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
];
|
||||
|
||||
export const defaults: BarGaugeOptions = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
orientation: VizOrientation.Horizontal,
|
||||
valueOptions: {
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
decimals: null,
|
||||
},
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
valueMappings: [],
|
||||
};
|
@ -9,6 +9,8 @@ import { FormField, PanelEditorProps } from '@grafana/ui';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
labelWidth = 8;
|
||||
|
||||
onToggleThresholdLabels = () =>
|
||||
this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
||||
@ -28,17 +30,17 @@ export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Gauge">
|
||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
|
||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
|
||||
<FormField label="Min value" labelWidth={this.labelWidth} onChange={this.onMinValueChange} value={minValue} />
|
||||
<FormField label="Max value" labelWidth={this.labelWidth} onChange={this.onMaxValueChange} value={maxValue} />
|
||||
<Switch
|
||||
label="Show labels"
|
||||
labelClass="width-8"
|
||||
labelClass={`width-${this.labelWidth}`}
|
||||
checked={showThresholdLabels}
|
||||
onChange={this.onToggleThresholdLabels}
|
||||
/>
|
||||
<Switch
|
||||
label="Show markers"
|
||||
labelClass="width-8"
|
||||
labelClass={`width-${this.labelWidth}`}
|
||||
checked={showThresholdMarkers}
|
||||
onChange={this.onToggleThresholdMarkers}
|
||||
/>
|
||||
|
@ -1,80 +1,59 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processTimeSeries, ThemeContext } from '@grafana/ui';
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { Gauge } from '@grafana/ui';
|
||||
import { Gauge, VizRepeater } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
|
||||
import { PanelProps, VizOrientation } from '@grafana/ui/src/types';
|
||||
|
||||
interface Props extends PanelProps<GaugeOptions> {}
|
||||
interface State {
|
||||
value: TimeSeriesValue;
|
||||
}
|
||||
|
||||
export class GaugePanel extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.findValue(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.data !== prevProps.data) {
|
||||
this.setState({ value: this.findValue(this.props) });
|
||||
}
|
||||
}
|
||||
|
||||
findValue(props: Props): number | null {
|
||||
const { data, options } = props;
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
renderGauge(value, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
if (data) {
|
||||
const vmSeries = processTimeSeries({
|
||||
data,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
if (vmSeries[0]) {
|
||||
return vmSeries[0].stats[valueOptions.stat];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height, replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
const { value } = this.state;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => (
|
||||
<Gauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</ThemeContext.Consumer>
|
||||
<Gauge
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, options, height, width } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
data,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={VizOrientation.Auto}>
|
||||
{({ vizHeight, vizWidth, valueInfo }) => this.renderGauge(valueInfo.value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
PanelEditorProps,
|
||||
|
@ -8,3 +8,15 @@ export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
|
||||
|
||||
reactPanel.setEditor(GaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler((pluginId: string, prevOptions: any) => {
|
||||
const options: Partial<GaugeOptions> = {};
|
||||
|
||||
if (prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
@ -31,5 +31,5 @@ export const defaults: GaugeOptions = {
|
||||
unit: 'none',
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
};
|
||||
|
@ -174,9 +174,11 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
|
||||
onToggleAxis={this.props.onToggleAxis}
|
||||
enableNamedColors
|
||||
>
|
||||
<span className="graph-legend-icon">
|
||||
<SeriesIcon color={this.props.color} />
|
||||
</span>
|
||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||
<span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
|
||||
<SeriesIcon color={this.props.color} />
|
||||
</span>
|
||||
)}
|
||||
</SeriesColorPicker>
|
||||
);
|
||||
}
|
||||
|
@ -67,6 +67,17 @@
|
||||
<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right">
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6">X-Min</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="ctrl.panel.xaxis.min" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6">X-Max</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="ctrl.panel.xaxis.max" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<br/>
|
||||
<h5 class="section-heading">Y-Axes</h5>
|
||||
|
@ -337,9 +337,17 @@ class GraphElement {
|
||||
let bucketSize: number;
|
||||
|
||||
if (this.data.length) {
|
||||
const histMin = _.min(_.map(this.data, s => s.stats.min));
|
||||
const histMax = _.max(_.map(this.data, s => s.stats.max));
|
||||
let histMin = _.min(_.map(this.data, s => s.stats.min));
|
||||
let histMax = _.max(_.map(this.data, s => s.stats.max));
|
||||
const ticks = panel.xaxis.buckets || this.panelWidth / 50;
|
||||
if (panel.xaxis.min != null) {
|
||||
const isInvalidXaxisMin = tickStep(panel.xaxis.min, histMax, ticks) <= 0;
|
||||
histMin = isInvalidXaxisMin ? histMin : panel.xaxis.min;
|
||||
}
|
||||
if (panel.xaxis.max != null) {
|
||||
const isInvalidXaxisMax = tickStep(histMin, panel.xaxis.max, ticks) <= 0;
|
||||
histMax = isInvalidXaxisMax ? histMax : panel.xaxis.max;
|
||||
}
|
||||
bucketSize = tickStep(histMin, histMax, ticks);
|
||||
options.series.bars.barWidth = bucketSize * 0.8;
|
||||
this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
|
||||
|
@ -43,6 +43,10 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m
|
||||
}
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
// filter out values outside the min and max boundaries
|
||||
if (values[i] < min || values[i] > max) {
|
||||
continue;
|
||||
}
|
||||
const bound = getBucketBound(values[i], bucketSize);
|
||||
histogram[bound] = histogram[bound] + 1;
|
||||
}
|
||||
|
@ -516,4 +516,408 @@ describe('grafanaGraph', () => {
|
||||
expect(ctx.plotData[0].data[0][1]).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is set', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 150;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain values lower than min', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is zero', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 0;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain values lower than zero', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is null', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = null;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is undefined', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = undefined;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is set', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = 250;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain values greater than max', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is zero', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = 0;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain values greater than zero', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is null', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = null;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis max should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is undefined', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = undefined;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis max should not should node affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min and max are set', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 150;
|
||||
ctrl.panel.xaxis.max = 250;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain values lower than min and greater than max', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min and max are zero', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 0;
|
||||
ctrl.panel.xaxis.max = 0;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis max should be ignored otherwise the bucketSize is zero', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min and max are null', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = null;
|
||||
ctrl.panel.xaxis.max = null;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min and max should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min and max are undefined', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = undefined;
|
||||
ctrl.panel.xaxis.max = undefined;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min and max should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is greater than xaxis max', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 150;
|
||||
ctrl.panel.xaxis.max = 100;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis max should be ignored otherwise the bucketSize is negative', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
// aaa
|
||||
describe('when graph is histogram, and xaxis min is greater than the maximum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 301;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min should be ignored otherwise the bucketSize is negative', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is equal to the maximum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 300;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min should be ignored otherwise the bucketSize is zero', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis min is lower than the minimum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.min = 99;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('xaxis min should not affect the histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is equal to the minimum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = 100;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate correct histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is a lower than the minimum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = 99;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate empty histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(nonZero.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when graph is histogram, and xaxis max is greater than the maximum value', () => {
|
||||
beforeEach(() => {
|
||||
setupCtx(() => {
|
||||
ctrl.panel.xaxis.mode = 'histogram';
|
||||
ctrl.panel.xaxis.max = 301;
|
||||
ctrl.panel.stack = false;
|
||||
ctrl.hiddenSeries = {};
|
||||
ctx.data[0] = new TimeSeries({
|
||||
datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
|
||||
alias: 'series1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate correct histogram', () => {
|
||||
const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
|
||||
expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
|
||||
expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,6 +32,7 @@ export class AxesEditorCtrl {
|
||||
Auto: 'auto',
|
||||
Upper: 'upper',
|
||||
Lower: 'lower',
|
||||
Middle: 'middle',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ const LEGEND_HEIGHT_PX = 6;
|
||||
const LEGEND_WIDTH_PX = 100;
|
||||
const LEGEND_TICK_SIZE = 0;
|
||||
const LEGEND_VALUE_MARGIN = 0;
|
||||
const LEGEND_PADDING_LEFT = 10;
|
||||
const LEGEND_SEGMENT_WIDTH = 10;
|
||||
|
||||
/**
|
||||
* Color legend for heatmap editor.
|
||||
@ -19,7 +21,7 @@ coreModule.directive('colorLegend', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
|
||||
link: (scope, elem, attrs) => {
|
||||
link: (scope: any, elem, attrs) => {
|
||||
const ctrl = scope.ctrl;
|
||||
const panel = scope.ctrl.panel;
|
||||
|
||||
@ -55,7 +57,7 @@ coreModule.directive('heatmapLegend', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
|
||||
link: (scope, elem, attrs) => {
|
||||
link: (scope: any, elem, attrs) => {
|
||||
const ctrl = scope.ctrl;
|
||||
const panel = scope.ctrl.panel;
|
||||
|
||||
@ -67,10 +69,11 @@ coreModule.directive('heatmapLegend', () => {
|
||||
function render() {
|
||||
clearLegend(elem);
|
||||
if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
|
||||
const rangeFrom = 0;
|
||||
const rangeTo = ctrl.data.cardStats.max;
|
||||
const maxValue = panel.color.max || rangeTo;
|
||||
const minValue = panel.color.min || 0;
|
||||
const cardStats = ctrl.data.cardStats;
|
||||
const rangeFrom = _.isNil(panel.color.min) ? Math.min(cardStats.min, 0) : panel.color.min;
|
||||
const rangeTo = _.isNil(panel.color.max) ? cardStats.max : panel.color.max;
|
||||
const maxValue = cardStats.max;
|
||||
const minValue = cardStats.min;
|
||||
|
||||
if (panel.color.mode === 'spectrum') {
|
||||
const colorScheme = _.find(ctrl.colorSchemes, {
|
||||
@ -95,27 +98,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
|
||||
const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
const legendHeight = legendElem.attr('height');
|
||||
|
||||
let rangeStep = 1;
|
||||
if (rangeTo - rangeFrom > legendWidth) {
|
||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
||||
}
|
||||
const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
|
||||
const widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
|
||||
legend
|
||||
.append('g')
|
||||
.attr('class', 'legend-color-bar')
|
||||
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
|
||||
.selectAll('.heatmap-color-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d * widthFactor)
|
||||
.attr('x', d => Math.round((d - rangeFrom) * widthFactor))
|
||||
.attr('y', 0)
|
||||
.attr('width', rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
||||
.attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', d => colorScale(d));
|
||||
|
||||
drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
||||
drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
|
||||
}
|
||||
|
||||
function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
|
||||
@ -126,31 +129,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
||||
const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
const legendHeight = legendElem.attr('height');
|
||||
|
||||
let rangeStep = 1;
|
||||
if (rangeTo - rangeFrom > legendWidth) {
|
||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
||||
}
|
||||
const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
|
||||
const widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
const opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||
legend
|
||||
.append('g')
|
||||
.attr('class', 'legend-color-bar')
|
||||
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
|
||||
.selectAll('.heatmap-opacity-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d * widthFactor)
|
||||
.attr('x', d => Math.round((d - rangeFrom) * widthFactor))
|
||||
.attr('y', 0)
|
||||
.attr('width', rangeStep * widthFactor)
|
||||
.attr('width', Math.round(rangeStep * widthFactor))
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', options.cardColor)
|
||||
.style('opacity', d => opacityScale(d));
|
||||
|
||||
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
||||
drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
|
||||
}
|
||||
|
||||
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
|
||||
function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange) {
|
||||
const legendElem = $(elem).find('svg');
|
||||
const legend = d3.select(legendElem.get(0));
|
||||
|
||||
@ -160,10 +163,10 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
|
||||
|
||||
const legendValueScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, rangeTo])
|
||||
.domain([rangeFrom, rangeTo])
|
||||
.range([0, legendWidth]);
|
||||
|
||||
const ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
|
||||
const ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
|
||||
const xAxis = d3
|
||||
.axisBottom(legendValueScale)
|
||||
.tickValues(ticks)
|
||||
@ -171,7 +174,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
|
||||
|
||||
const colorRect = legendElem.find(':first-child');
|
||||
const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
|
||||
const posX = getSvgElemX(colorRect);
|
||||
const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
|
||||
|
||||
d3.select(legendElem.get(0))
|
||||
.append('g')
|
||||
@ -284,11 +287,12 @@ function getSvgElemHeight(elem) {
|
||||
function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
|
||||
const range = rangeTo - rangeFrom;
|
||||
const tickStepSize = tickStep(rangeFrom, rangeTo, 3);
|
||||
const ticksNum = Math.round(range / tickStepSize);
|
||||
const ticksNum = Math.ceil(range / tickStepSize);
|
||||
const firstTick = getFirstCloseTick(rangeFrom, tickStepSize);
|
||||
let ticks = [];
|
||||
|
||||
for (let i = 0; i < ticksNum; i++) {
|
||||
const current = tickStepSize * i;
|
||||
const current = firstTick + tickStepSize * i;
|
||||
// Add user-defined min and max if it had been set
|
||||
if (isValueCloseTo(minValue, current, tickStepSize)) {
|
||||
ticks.push(minValue);
|
||||
@ -302,7 +306,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
|
||||
} else if (maxValue < current) {
|
||||
ticks.push(maxValue);
|
||||
}
|
||||
ticks.push(tickStepSize * i);
|
||||
ticks.push(current);
|
||||
}
|
||||
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
|
||||
ticks.push(maxValue);
|
||||
@ -316,3 +320,10 @@ function isValueCloseTo(val, valueTo, step) {
|
||||
const diff = Math.abs(val - valueTo);
|
||||
return diff < step * 0.3;
|
||||
}
|
||||
|
||||
function getFirstCloseTick(minValue, step) {
|
||||
if (minValue < 0) {
|
||||
return Math.floor(minValue / step) * step;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ const panelDefaults = {
|
||||
},
|
||||
dataFormat: 'timeseries',
|
||||
yBucketBound: 'auto',
|
||||
reverseYBuckets: false,
|
||||
xAxis: {
|
||||
show: true,
|
||||
},
|
||||
@ -55,6 +56,7 @@ const panelDefaults = {
|
||||
showHistogram: false,
|
||||
},
|
||||
highlightCards: true,
|
||||
hideZeroBuckets: false,
|
||||
};
|
||||
|
||||
const colorModes = ['opacity', 'spectrum'];
|
||||
@ -97,7 +99,7 @@ const colorSchemes = [
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
|
||||
];
|
||||
|
||||
const dsSupportHistogramSort = ['prometheus', 'elasticsearch'];
|
||||
const dsSupportHistogramSort = ['elasticsearch'];
|
||||
|
||||
export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
@ -108,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
selectionActivated: boolean;
|
||||
unitFormats: any;
|
||||
data: any;
|
||||
series: any;
|
||||
series: any[];
|
||||
timeSrv: any;
|
||||
dataWarning: any;
|
||||
decimals: number;
|
||||
@ -146,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onRender() {
|
||||
if (!this.range) {
|
||||
if (!this.range || !this.series) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -204,7 +206,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
yBucketSize = 1;
|
||||
}
|
||||
|
||||
const { cards, cardStats } = convertToCards(bucketsData);
|
||||
const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
|
||||
|
||||
this.data = {
|
||||
buckets: bucketsData,
|
||||
@ -225,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.series.sort(sortSeriesByLabel);
|
||||
}
|
||||
|
||||
if (this.panel.reverseYBuckets) {
|
||||
this.series.reverse();
|
||||
}
|
||||
|
||||
// Convert histogram to heatmap. Each histogram bucket represented by the series which name is
|
||||
// a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels.
|
||||
// a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as Y axis labels.
|
||||
bucketsData = histogramToHeatmap(this.series);
|
||||
|
||||
tsBuckets = _.map(this.series, 'label');
|
||||
const yBucketBound = this.panel.yBucketBound;
|
||||
if ((panelDatasource === 'prometheus' && yBucketBound !== 'lower') || yBucketBound === 'upper') {
|
||||
if (
|
||||
(panelDatasource === 'prometheus' && yBucketBound !== 'lower' && yBucketBound !== 'middle') ||
|
||||
yBucketBound === 'upper'
|
||||
) {
|
||||
// Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
|
||||
tsBuckets = [''].concat(tsBuckets);
|
||||
} else {
|
||||
@ -246,7 +255,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
// Always let yBucketSize=1 in 'tsbuckets' mode
|
||||
yBucketSize = 1;
|
||||
|
||||
const { cards, cardStats } = convertToCards(bucketsData);
|
||||
const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
|
||||
|
||||
this.data = {
|
||||
buckets: bucketsData,
|
||||
|
@ -93,25 +93,43 @@ function parseHistogramLabel(label: string): number {
|
||||
return value;
|
||||
}
|
||||
|
||||
interface HeatmapCard {
|
||||
x: number;
|
||||
y: number;
|
||||
yBounds: {
|
||||
top: number | null;
|
||||
bottom: number | null;
|
||||
};
|
||||
values: number[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface HeatmapCardStats {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert buckets into linear array of "cards" - objects, represented heatmap elements.
|
||||
* @param {Object} buckets
|
||||
* @return {Array} Array of "card" objects
|
||||
* @return {Object} Array of "card" objects and stats
|
||||
*/
|
||||
function convertToCards(buckets) {
|
||||
function convertToCards(buckets: any, hideZero = false): { cards: HeatmapCard[]; cardStats: HeatmapCardStats } {
|
||||
let min = 0,
|
||||
max = 0;
|
||||
const cards = [];
|
||||
const cards: HeatmapCard[] = [];
|
||||
_.forEach(buckets, xBucket => {
|
||||
_.forEach(xBucket.buckets, yBucket => {
|
||||
const card = {
|
||||
const card: HeatmapCard = {
|
||||
x: xBucket.x,
|
||||
y: yBucket.y,
|
||||
yBounds: yBucket.bounds,
|
||||
values: yBucket.values,
|
||||
count: yBucket.count,
|
||||
};
|
||||
cards.push(card);
|
||||
if (!hideZero || card.count !== 0) {
|
||||
cards.push(card);
|
||||
}
|
||||
|
||||
if (cards.length === 1) {
|
||||
min = yBucket.count;
|
||||
|
@ -114,7 +114,9 @@ export class HeatmapTooltip {
|
||||
};
|
||||
|
||||
boundBottom = tickFormatter(yBucketIndex);
|
||||
boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
|
||||
if (this.panel.yBucketBound !== 'middle') {
|
||||
boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
|
||||
}
|
||||
} else {
|
||||
// Display 0 if bucket is a special 'zero' bucket
|
||||
const bottom = yData.y ? yData.bounds.bottom : 0;
|
||||
@ -122,8 +124,9 @@ export class HeatmapTooltip {
|
||||
boundTop = bucketBoundFormatter(yData.bounds.top);
|
||||
}
|
||||
valuesNumber = countValueFormatter(yData.count);
|
||||
const boundStr = boundTop && boundBottom ? `${boundBottom} - ${boundTop}` : boundBottom || boundTop;
|
||||
tooltipHtml += `<div>
|
||||
bucket: <b>${boundBottom} - ${boundTop}</b> <br>
|
||||
bucket: <b>${boundStr}</b> <br>
|
||||
count: <b>${valuesNumber}</b> <br>
|
||||
</div>`;
|
||||
} else {
|
||||
|
@ -40,6 +40,11 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch ng-if="ctrl.panel.dataFormat == 'tsbuckets'"
|
||||
class="gf-form" label-class="width-8"
|
||||
label="Reverse order"
|
||||
checked="ctrl.panel.reverseYBuckets" on-change="ctrl.refresh()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group" ng-if="ctrl.panel.dataFormat == 'timeseries'">
|
||||
|
@ -63,6 +63,10 @@
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Buckets</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Hide zero"
|
||||
checked="ctrl.panel.hideZeroBuckets" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Space</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardPadding" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
|
@ -379,6 +379,12 @@ export class HeatmapRenderer {
|
||||
const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
|
||||
this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
|
||||
|
||||
if (this.panel.yBucketBound === 'middle' && tickValues && tickValues.length) {
|
||||
// Shift Y axis labels to the middle of bucket
|
||||
const tickShift = 0 - this.chartHeight / (tickValues.length - 1) / 2;
|
||||
this.heatmap.selectAll('.axis-y text').attr('transform', 'translate(' + 0 + ',' + tickShift + ')');
|
||||
}
|
||||
|
||||
// Remove vertical line in the right of axis labels (called domain in d3)
|
||||
this.heatmap
|
||||
.select('.axis-y')
|
||||
@ -518,14 +524,16 @@ export class HeatmapRenderer {
|
||||
}
|
||||
|
||||
const cardsData = this.data.cards;
|
||||
const maxValueAuto = this.data.cardStats.max;
|
||||
const maxValue = this.panel.color.max || maxValueAuto;
|
||||
const minValue = this.panel.color.min || 0;
|
||||
const cardStats = this.data.cardStats;
|
||||
const maxValueAuto = cardStats.max;
|
||||
const minValueAuto = Math.min(cardStats.min, 0);
|
||||
const maxValue = _.isNil(this.panel.color.max) ? maxValueAuto : this.panel.color.max;
|
||||
const minValue = _.isNil(this.panel.color.min) ? minValueAuto : this.panel.color.min;
|
||||
const colorScheme = _.find(this.ctrl.colorSchemes, {
|
||||
value: this.panel.color.colorScheme,
|
||||
});
|
||||
this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
|
||||
this.opacityScale = getOpacityScale(this.panel.color, maxValue);
|
||||
this.opacityScale = getOpacityScale(this.panel.color, maxValue, minValue);
|
||||
this.setCardSize();
|
||||
|
||||
let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
|
||||
@ -615,8 +623,8 @@ export class HeatmapRenderer {
|
||||
w = this.cardWidth;
|
||||
}
|
||||
|
||||
// Card width should be MIN_CARD_SIZE at least
|
||||
w = Math.max(w, MIN_CARD_SIZE);
|
||||
// Card width should be MIN_CARD_SIZE at least, but cut cards shouldn't be displayed
|
||||
w = w > 0 ? Math.max(w, MIN_CARD_SIZE) : 0;
|
||||
return w;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, stringToJsRegex } from '@grafana/ui';
|
||||
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
|
||||
|
||||
export class TableRenderer {
|
||||
formatters: any[];
|
||||
@ -50,7 +51,7 @@ export class TableRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
getColorForValue(value, style) {
|
||||
getColorForValue(value, style: ColumnStyle) {
|
||||
if (!style.thresholds) {
|
||||
return null;
|
||||
}
|
||||
@ -62,7 +63,7 @@ export class TableRenderer {
|
||||
return getColorFromHexRgbOrName(_.first(style.colors), this.theme);
|
||||
}
|
||||
|
||||
defaultCellFormatter(v, style) {
|
||||
defaultCellFormatter(v, style: ColumnStyle) {
|
||||
if (v === null || v === void 0 || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
@ -189,7 +190,7 @@ export class TableRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
setColorState(value, style) {
|
||||
setColorState(value, style: ColumnStyle) {
|
||||
if (!style.colorMode) {
|
||||
return;
|
||||
}
|
||||
|
9
public/app/plugins/panel/table2/README.md
Normal file
9
public/app/plugins/panel/table2/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Table Panel - Native Plugin
|
||||
|
||||
The Table Panel is **included** with Grafana.
|
||||
|
||||
The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
|
||||
|
||||
Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here:
|
||||
|
||||
[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/)
|
29
public/app/plugins/panel/table2/TablePanel.tsx
Normal file
29
public/app/plugins/panel/table2/TablePanel.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelProps, ThemeContext } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
import Table from '@grafana/ui/src/components/Table/Table';
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class TablePanel extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, options } = this.props;
|
||||
|
||||
if (data.length < 1) {
|
||||
return <div>No Table Data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
55
public/app/plugins/panel/table2/TablePanelEditor.tsx
Normal file
55
public/app/plugins/panel/table2/TablePanelEditor.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
//// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelEditorProps, Switch, FormField } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
||||
onToggleShowHeader = () => {
|
||||
this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader });
|
||||
};
|
||||
|
||||
onToggleFixedHeader = () => {
|
||||
this.props.onOptionsChange({ ...this.props.options, fixedHeader: !this.props.options.fixedHeader });
|
||||
};
|
||||
|
||||
onToggleRotate = () => {
|
||||
this.props.onOptionsChange({ ...this.props.options, rotate: !this.props.options.rotate });
|
||||
};
|
||||
|
||||
onFixedColumnsChange = ({ target }) => {
|
||||
this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Header</h5>
|
||||
<Switch label="Show" labelClass="width-6" checked={showHeader} onChange={this.onToggleShowHeader} />
|
||||
<Switch label="Fixed" labelClass="width-6" checked={fixedHeader} onChange={this.onToggleFixedHeader} />
|
||||
</div>
|
||||
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Display</h5>
|
||||
<Switch label="Rotate" labelClass="width-8" checked={rotate} onChange={this.onToggleRotate} />
|
||||
<FormField
|
||||
label="Fixed Columns"
|
||||
labelWidth={8}
|
||||
inputWidth={4}
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
onChange={this.onFixedColumnsChange}
|
||||
value={fixedColumns}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
67
public/app/plugins/panel/table2/img/icn-table-panel.svg
Normal file
67
public/app/plugins/panel/table2/img/icn-table-panel.svg
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="15.8113" y1="25" x2="15.8113" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="0" style="fill:url(#SVGID_1_);" width="31.623" height="15.049"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="50" y1="25" x2="50" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="34.188" style="fill:url(#SVGID_2_);" width="31.623" height="15.049"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="84.1887" y1="25" x2="84.1887" y2="-2.5362">
|
||||
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||
</linearGradient>
|
||||
<rect x="68.377" style="fill:url(#SVGID_3_);" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="16.99" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="33.981" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="50.971" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="67.961" style="fill:#6D6E71;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="34.188" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
<rect x="68.377" y="84.951" style="fill:#898989;" width="31.623" height="15.049"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
9
public/app/plugins/panel/table2/module.tsx
Normal file
9
public/app/plugins/panel/table2/module.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import { TablePanelEditor } from './TablePanelEditor';
|
||||
import { TablePanel } from './TablePanel';
|
||||
import { Options, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(TablePanel);
|
||||
reactPanel.setEditor(TablePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
19
public/app/plugins/panel/table2/plugin.json
Normal file
19
public/app/plugins/panel/table2/plugin.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "React Table",
|
||||
"id": "table2",
|
||||
"state": "alpha",
|
||||
|
||||
"dataFormats": ["table"],
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-table-panel.svg",
|
||||
"large": "img/icn-table-panel.svg"
|
||||
}
|
||||
}
|
||||
}
|
35
public/app/plugins/panel/table2/types.ts
Normal file
35
public/app/plugins/panel/table2/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
|
||||
|
||||
export interface Options {
|
||||
showHeader: boolean;
|
||||
fixedHeader: boolean;
|
||||
fixedColumns: number;
|
||||
rotate: boolean;
|
||||
|
||||
styles: ColumnStyle[];
|
||||
}
|
||||
|
||||
export const defaults: Options = {
|
||||
showHeader: true,
|
||||
fixedHeader: true,
|
||||
fixedColumns: 0,
|
||||
rotate: false,
|
||||
styles: [
|
||||
{
|
||||
type: 'date',
|
||||
pattern: 'Time',
|
||||
alias: 'Time',
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
{
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
alias: '',
|
||||
decimals: 2,
|
||||
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
thresholds: [],
|
||||
},
|
||||
],
|
||||
};
|
@ -4,13 +4,14 @@ import {
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
DataQuery,
|
||||
DataQueryResponseData,
|
||||
DataSourceSelectItem,
|
||||
DataSourceApi,
|
||||
QueryHint,
|
||||
ExploreStartPageProps,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { Emitter, TimeSeries } from 'app/core/core';
|
||||
import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model';
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
@ -322,6 +323,12 @@ export interface QueryTransaction {
|
||||
|
||||
export type RangeScanner = () => RawTimeRange;
|
||||
|
||||
export type ResultGetter = (
|
||||
result: DataQueryResponseData,
|
||||
transaction: QueryTransaction,
|
||||
allTransactions: QueryTransaction[]
|
||||
) => TimeSeries;
|
||||
|
||||
export interface TextMatch {
|
||||
text: string;
|
||||
start: number;
|
||||
|
@ -20,6 +20,12 @@ $enable-hover-media-query: false !default;
|
||||
// Control the default styling of most Bootstrap elements by modifying these
|
||||
// variables. Mostly focused on spacing.
|
||||
|
||||
$space-xxs: 2px !default;
|
||||
$space-xs: 4px !default;
|
||||
$space-sm: 8px !default;
|
||||
$space-md: 16px !default;
|
||||
$space-lg: 24px !default;
|
||||
$space-xl: 32px !default;
|
||||
$spacer: 14px !default;
|
||||
$spacer-x: $spacer !default;
|
||||
$spacer-y: $spacer !default;
|
||||
@ -49,7 +55,6 @@ $spacers: (
|
||||
),
|
||||
),
|
||||
) !default;
|
||||
$border-width: 1px !default;
|
||||
|
||||
// Grid breakpoints
|
||||
//
|
||||
@ -82,14 +87,11 @@ $container-max-widths: (
|
||||
$grid-columns: 12 !default;
|
||||
$grid-gutter-width: 30px !default;
|
||||
|
||||
$enable-flex: true;
|
||||
|
||||
// Typography
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
$font-size-root: 14px !default;
|
||||
$font-size-base: 13px !default;
|
||||
@ -100,7 +102,9 @@ $font-size-sm: 12px !default;
|
||||
$font-size-xs: 10px !default;
|
||||
|
||||
$line-height-base: 1.5 !default;
|
||||
$font-weight-semi-bold: 500;
|
||||
|
||||
$font-weight-regular: 400 !default;
|
||||
$font-weight-semi-bold: 500 !default;
|
||||
|
||||
$font-size-h1: 28px !default;
|
||||
$font-size-h2: 24px !default;
|
||||
@ -109,20 +113,14 @@ $font-size-h4: 18px !default;
|
||||
$font-size-h5: 16px !default;
|
||||
$font-size-h6: 14px !default;
|
||||
|
||||
$headings-margin-bottom: ($spacer / 2) !default;
|
||||
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$headings-font-weight: 400 !default;
|
||||
$headings-line-height: 1.1 !default;
|
||||
|
||||
$hr-border-width: $border-width !default;
|
||||
$dt-font-weight: bold !default;
|
||||
|
||||
// Components
|
||||
//
|
||||
// Define common padding and border radius sizes and more.
|
||||
|
||||
$line-height-lg: (4 / 3) !default;
|
||||
$line-height-sm: 1.5 !default;
|
||||
$border-width: 1px !default;
|
||||
|
||||
$border-radius: 3px !default;
|
||||
$border-radius-lg: 5px !default;
|
||||
@ -149,23 +147,17 @@ $input-padding-x: 10px !default;
|
||||
$input-padding-y: 8px !default;
|
||||
$input-line-height: 18px !default;
|
||||
|
||||
$input-btn-border-width: 1px;
|
||||
$input-border-radius: 0 $border-radius $border-radius 0 !default;
|
||||
$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
|
||||
|
||||
$label-border-radius: $border-radius 0 0 $border-radius !default;
|
||||
$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
|
||||
|
||||
$input-padding-y-sm: 4px !default;
|
||||
|
||||
$input-padding-x-lg: 20px !default;
|
||||
$input-padding-y-lg: 10px !default;
|
||||
|
||||
$input-height: 35px !default;
|
||||
|
||||
$gf-form-margin: 3px;
|
||||
$gf-form-input-height: 35px;
|
||||
|
||||
$cursor-disabled: not-allowed !default;
|
||||
|
||||
// Form validation icons
|
||||
@ -202,16 +194,13 @@ $btn-padding-y-lg: 11px !default;
|
||||
$btn-padding-x-xl: 21px !default;
|
||||
$btn-padding-y-xl: 11px !default;
|
||||
|
||||
$btn-border-radius: 2px;
|
||||
|
||||
$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
|
||||
// sidemenu
|
||||
$side-menu-width: 60px;
|
||||
|
||||
// dashboard
|
||||
$panel-margin: 10px;
|
||||
$dashboard-padding: $panel-margin * 2;
|
||||
$dashboard-padding: 10px * 2;
|
||||
$panel-horizontal-padding: 10;
|
||||
$panel-vertical-padding: 5;
|
||||
$panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
|
||||
@ -246,8 +235,3 @@ $external-services: (
|
||||
icon: '',
|
||||
),
|
||||
) !default;
|
||||
|
||||
:export {
|
||||
panelhorizontalpadding: $panel-horizontal-padding;
|
||||
panelverticalpadding: $panel-vertical-padding;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user