Merge pull request #15529 from grafana/cp-6.0.0-beta3

Cherry picks for v6.0.0-beta3
This commit is contained in:
Leonard Gram
2019-02-19 11:32:29 +01:00
committed by GitHub
68 changed files with 874 additions and 511 deletions
+6 -17
View File
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.0.0-beta2",
"version": "6.0.0-beta3",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -121,7 +121,7 @@
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"storybook": "cd packages/grafana-ui && yarn storybook",
"prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\""
},
"husky": {
"hooks": {
@@ -129,14 +129,8 @@
}
},
"lint-staged": {
"*.{ts,tsx,json,scss}": [
"prettier --write",
"git add"
],
"*pkg/**/*.go": [
"gofmt -w -s",
"git add"
]
"*.{ts,tsx,json,scss}": ["prettier --write", "git add"],
"*pkg/**/*.go": ["gofmt -w -s", "git add"]
},
"prettier": {
"trailingComma": "es5",
@@ -201,12 +195,7 @@
"**/@types/react": "16.7.6"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/@types/*",
"**/@types/*/**"
]
"packages": ["packages/*"],
"nohoist": ["**/@types/*", "**/@types/*/**"]
}
}
@@ -84,9 +84,9 @@ describe('Get thresholds formatted', () => {
it('should get the correct formatted values when thresholds are added', () => {
const { instance } = setup({
thresholds: [
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
],
});
@@ -9,7 +9,7 @@ import { Themeable } from '../../index';
type TimeSeriesValue = string | number | null;
export interface Props extends Themeable {
decimals: number;
decimals?: number | null;
height: number;
valueMappings: ValueMapping[];
maxValue: number;
@@ -98,16 +98,15 @@ export class Gauge extends PureComponent<Props> {
getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props;
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
const lastThreshold = thresholds[thresholds.length - 1];
return [
...thresholdsSortedByIndex.map(threshold => {
...thresholds.map(threshold => {
if (threshold.index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
}
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
const previousThreshold = thresholds[threshold.index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
@@ -22,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
showBars: false,
};
element: HTMLElement | null;
element: HTMLElement | null = null;
componentDidUpdate() {
this.draw();
@@ -1,9 +1,8 @@
import React, { ChangeEvent } from 'react';
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor';
const setup = (propOverrides?: object) => {
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
onChange: jest.fn(),
thresholds: [],
@@ -11,12 +10,26 @@ const setup = (propOverrides?: object) => {
Object.assign(props, propOverrides);
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
const wrapper = mount(<ThresholdsEditor {...props} />);
const instance = wrapper.instance() as ThresholdsEditor;
return {
instance,
wrapper,
};
};
describe('Render', () => {
it('should render with base threshold', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
});
describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const instance = setup();
const { instance } = setup();
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
});
@@ -24,7 +37,7 @@ describe('Initialization', () => {
describe('Add threshold', () => {
it('should not add threshold at index 0', () => {
const instance = setup();
const { instance } = setup();
instance.onAddThreshold(0);
@@ -32,32 +45,32 @@ describe('Add threshold', () => {
});
it('should add threshold', () => {
const instance = setup();
const { instance } = setup();
instance.onAddThreshold(1);
expect(instance.state.thresholds).toEqual([
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
]);
});
it('should add another threshold above a first', () => {
const instance = setup({
const { instance } = setup({
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
});
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
]);
});
it('should add another threshold between first and second index', () => {
const instance = setup({
const { instance } = setup({
thresholds: [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
@@ -68,10 +81,10 @@ describe('Add threshold', () => {
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
{ index: 3, value: 75, color: '#6ED0E0' },
{ index: 2, value: 62.5, color: '#EF843C' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 62.5, color: '#EF843C' },
{ index: 3, value: 75, color: '#6ED0E0' },
]);
});
});
@@ -83,7 +96,7 @@ describe('Remove threshold', () => {
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({ thresholds });
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(thresholds[0]);
@@ -96,9 +109,7 @@ describe('Remove threshold', () => {
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({
thresholds,
});
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(thresholds[1]);
@@ -116,7 +127,7 @@ describe('change threshold value', () => {
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({ thresholds });
const { instance } = setup({ thresholds });
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
@@ -126,7 +137,7 @@ describe('change threshold value', () => {
});
it('should update value', () => {
const instance = setup();
const { instance } = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
@@ -150,24 +161,24 @@ describe('change threshold value', () => {
});
describe('on blur threshold value', () => {
it('should resort rows and update indexes', () => {
const instance = setup();
it.only('should resort rows and update indexes', () => {
const { instance } = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 78, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
instance.state = {
instance.setState({
thresholds,
};
});
instance.onBlur();
expect(instance.state.thresholds).toEqual([
{ index: 2, value: 78, color: '#EAB839' },
{ index: 1, value: 75, color: '#6ED0E0' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 75, color: '#6ED0E0' },
{ index: 2, value: 78, color: '#EAB839' },
]);
});
});
@@ -1,7 +1,7 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { ColorPicker } from '..';
import { PanelOptionsGroup } from '..';
import { colors } from '../../utils';
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
@@ -54,16 +54,16 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
// Set a color
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
const color = colors.filter(c => !newThresholds.some(t => t.color === c))[0];
this.setState(
{
thresholds: this.sortThresholds([
...newThresholds,
{
color,
index,
value: value as number,
color,
},
]),
},
@@ -137,10 +137,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
onBlur = () => {
this.setState(prevState => {
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
let index = sortThresholds.length - 1;
let index = 0;
sortThresholds.forEach(t => {
t.index = index--;
t.index = index++;
});
return { thresholds: sortThresholds };
});
@@ -153,12 +154,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
sortThresholds = (thresholds: Threshold[]) => {
return thresholds.sort((t1, t2) => {
return t2.value - t1.value;
return t1.value - t2.value;
});
};
renderInput = (threshold: Threshold) => {
const value = threshold.index === 0 ? 'Base' : threshold.value;
return (
<div className="thresholds-row-input-inner">
<span className="thresholds-row-input-inner-arrow" />
@@ -169,51 +169,60 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
</div>
)}
</div>
<div className="thresholds-row-input-inner-value">
<input
type="number"
step="0.0001"
onChange={event => this.onChangeThresholdValue(event, threshold)}
value={value}
onBlur={this.onBlur}
readOnly={threshold.index === 0}
/>
</div>
{threshold.index > 0 && (
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
<i className="fa fa-times" />
{threshold.index === 0 && (
<div className="thresholds-row-input-inner-value">
<input type="text" value="Base" readOnly />
</div>
)}
{threshold.index > 0 && (
<>
<div className="thresholds-row-input-inner-value">
<input
type="number"
step="0.0001"
onChange={event => this.onChangeThresholdValue(event, threshold)}
value={threshold.value}
onBlur={this.onBlur}
readOnly={threshold.index === 0}
/>
</div>
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
<i className="fa fa-times" />
</div>
</>
)}
</div>
);
};
render() {
const { thresholds } = this.state;
return (
<ThemeContext.Consumer>
{theme => {
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
{thresholds.map((threshold, index) => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<div
className="thresholds-row-add-button"
onClick={() => this.onAddThreshold(threshold.index + 1)}
>
<i className="fa fa-plus" />
{thresholds
.slice(0)
.reverse()
.map((threshold, index) => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<div
className="thresholds-row-add-button"
onClick={() => this.onAddThreshold(threshold.index + 1)}
>
<i className="fa fa-plus" />
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
);
})}
</div>
</PanelOptionsGroup>
);
@@ -43,7 +43,7 @@
}
.thresholds-row-input {
margin-top: 49px;
margin-top: 44px;
margin-left: 2px;
}
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render with base threshold 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
`;
@@ -35,8 +35,16 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
class Popper extends PureComponent<Props> {
render() {
const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
const { content } = this.props;
const {
content,
show,
placement,
onMouseEnter,
onMouseLeave,
className,
wrapperClassName,
renderArrow,
} = this.props;
return (
<Manager>
@@ -50,7 +58,7 @@ class Popper extends PureComponent<Props> {
// TODO: move modifiers config to popper controller
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
>
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
{({ ref, style, placement, arrowProps }) => {
return (
<div
onMouseEnter={onMouseEnter}
@@ -65,11 +73,7 @@ class Popper extends PureComponent<Props> {
className={`${wrapperClassName}`}
>
<div className={className}>
{typeof content === 'string'
? content
: React.cloneElement(content, {
updatePopperPosition: scheduleUpdate,
})}
{typeof content === 'string' ? content : React.cloneElement(content)}
{renderArrow &&
renderArrow({
arrowProps,
+1 -1
View File
@@ -6,7 +6,7 @@ let themeMock: ((name?: string) => GrafanaTheme) | null;
export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
export const mockTheme = (mock: (name: string) => GrafanaTheme) => {
export const mockTheme = (mock: (name?: string) => GrafanaTheme) => {
themeMock = mock;
return () => {
themeMock = null;
@@ -29,6 +29,16 @@ export interface DataQuery {
datasource?: string | null;
}
export interface DataQueryError {
data?: {
message?: string;
error?: string;
};
message?: string;
status?: string;
statusText?: string;
}
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
timezone: string;
range: TimeRange;
+20 -1
View File
@@ -1,3 +1,4 @@
import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data';
import { TimeRange } from './time';
@@ -19,11 +20,29 @@ export interface PanelData {
tableData?: TableData;
}
export interface PanelOptionsProps<T = any> {
export interface PanelEditorProps<T = any> {
options: T;
onChange: (options: T) => void;
}
export class ReactPanelPlugin<TOptions = any> {
panel: ComponentClass<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
defaults?: TOptions;
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
this.panel = panel;
}
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
this.editor = editor;
}
setDefaults(defaults: TOptions) {
this.defaults = defaults;
}
}
export interface PanelSize {
width: number;
height: number;
+2 -4
View File
@@ -1,5 +1,5 @@
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
import { ReactPanelPlugin } from './panel';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
@@ -81,9 +81,7 @@ export interface PluginExports {
// Panel plugin
PanelCtrl?: any;
Panel?: ComponentClass<PanelProps>;
PanelOptions?: ComponentClass<PanelOptionsProps>;
PanelDefaults?: any;
reactPanel: ReactPanelPlugin;
}
export interface PluginMeta {
@@ -1,20 +1,20 @@
import { toFixed } from './valueFormats';
import { toFixed, DecimalCount } from './valueFormats';
export function toPercent(size: number, decimals: number) {
export function toPercent(size: number, decimals: DecimalCount) {
if (size === null) {
return '';
}
return toFixed(size, decimals) + '%';
}
export function toPercentUnit(size: number, decimals: number) {
export function toPercentUnit(size: number, decimals: DecimalCount) {
if (size === null) {
return '';
}
return toFixed(100 * size, decimals) + '%';
}
export function toHex0x(value: number, decimals: number) {
export function toHex0x(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
@@ -25,7 +25,7 @@ export function toHex0x(value: number, decimals: number) {
return '0x' + hexString;
}
export function toHex(value: number, decimals: number) {
export function toHex(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
@@ -34,9 +34,9 @@ export function toHex(value: number, decimals: number) {
.toUpperCase();
}
export function sci(value: number, decimals: number) {
export function sci(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
return value.toExponential(decimals);
return value.toExponential(decimals as number);
}
@@ -1,4 +1,4 @@
import { toFixed, toFixedScaled } from './valueFormats';
import { toFixed, toFixedScaled, DecimalCount } from './valueFormats';
import moment from 'moment';
interface IntervalsInSeconds {
@@ -27,7 +27,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
[Interval.Millisecond]: 0.001,
};
export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) {
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -45,7 +45,7 @@ export function toNanoSeconds(size: number, decimals: number, scaledDecimals: nu
}
}
export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) {
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -59,7 +59,7 @@ export function toMicroSeconds(size: number, decimals: number, scaledDecimals: n
}
}
export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) {
export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -83,22 +83,29 @@ export function toMilliSeconds(size: number, decimals: number, scaledDecimals: n
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
}
export function toSeconds(size: number, decimals: number, scaledDecimals: number) {
export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount {
if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) {
return value1 - value2;
}
return undefined;
}
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
// Less than 1 µs, divide in ns
if (Math.abs(size) < 0.000001) {
return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns');
return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns');
}
// Less than 1 ms, divide in µs
if (Math.abs(size) < 0.001) {
return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs');
return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs');
}
// Less than 1 second, divide in ms
if (Math.abs(size) < 1) {
return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms');
return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms');
}
if (Math.abs(size) < 60) {
@@ -120,7 +127,7 @@ export function toSeconds(size: number, decimals: number, scaledDecimals: number
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
}
export function toMinutes(size: number, decimals: number, scaledDecimals: number) {
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -138,7 +145,7 @@ export function toMinutes(size: number, decimals: number, scaledDecimals: number
}
}
export function toHours(size: number, decimals: number, scaledDecimals: number) {
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -154,7 +161,7 @@ export function toHours(size: number, decimals: number, scaledDecimals: number)
}
}
export function toDays(size: number, decimals: number, scaledDecimals: number) {
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -168,13 +175,15 @@ export function toDays(size: number, decimals: number, scaledDecimals: number) {
}
}
export function toDuration(size: number, decimals: number, timeScale: Interval): string {
export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string {
if (size === null) {
return '';
}
if (size === 0) {
return '0 ' + timeScale + 's';
}
if (size < 0) {
return toDuration(-size, decimals, timeScale) + ' ago';
}
@@ -189,14 +198,22 @@ export function toDuration(size: number, decimals: number, timeScale: Interval):
{ long: Interval.Second },
{ long: Interval.Millisecond },
];
// convert $size to milliseconds
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
const strings = [];
// after first value >= 1 print only $decimals more
let decrementDecimals = false;
for (let i = 0; i < units.length && decimals >= 0; i++) {
let decimalsCount = 0;
if (decimals !== null || decimals !== undefined) {
decimalsCount = decimals as number;
}
for (let i = 0; i < units.length && decimalsCount >= 0; i++) {
const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
const value = size / interval;
if (value >= 1 || decrementDecimals) {
@@ -205,14 +222,14 @@ export function toDuration(size: number, decimals: number, timeScale: Interval):
const unit = units[i].long + (floor !== 1 ? 's' : '');
strings.push(floor + ' ' + unit);
size = size % interval;
decimals--;
decimalsCount--;
}
}
return strings.join(', ');
}
export function toClock(size: number, decimals?: number) {
export function toClock(size: number, decimals?: DecimalCount) {
if (size === null) {
return '';
}
@@ -257,11 +274,11 @@ export function toClock(size: number, decimals?: number) {
return format ? `${hours}:${moment.utc(size).format(format)}` : hours;
}
export function toDurationInMilliseconds(size: number, decimals: number) {
export function toDurationInMilliseconds(size: number, decimals: DecimalCount) {
return toDuration(size, decimals, Interval.Millisecond);
}
export function toDurationInSeconds(size: number, decimals: number) {
export function toDurationInSeconds(size: number, decimals: DecimalCount) {
return toDuration(size, decimals, Interval.Second);
}
@@ -276,19 +293,19 @@ export function toDurationInHoursMinutesSeconds(size: number) {
return strings.join(':');
}
export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) {
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) {
return toSeconds(size, decimals, scaledDecimals);
}
export function toClockMilliseconds(size: number, decimals: number) {
export function toClockMilliseconds(size: number, decimals: DecimalCount) {
return toClock(size, decimals);
}
export function toClockSeconds(size: number, decimals: number) {
export function toClockSeconds(size: number, decimals: DecimalCount) {
return toClock(size * 1000, decimals);
}
export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? moment.utc(value) : moment(value);
if (moment().isSame(value, 'day')) {
@@ -297,7 +314,7 @@ export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: n
return time.format('YYYY-MM-DD HH:mm:ss');
}
export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? moment.utc(value) : moment(value);
if (moment().isSame(value, 'day')) {
@@ -306,7 +323,7 @@ export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: nu
return time.format('MM/DD/YYYY h:mm:ss a');
}
export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? moment.utc(value) : moment(value);
return time.fromNow();
}
@@ -1,9 +1,9 @@
import { scaledUnits } from './valueFormats';
import { scaledUnits, DecimalCount } from './valueFormats';
export function currency(symbol: string) {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals: number, scaledDecimals: number) => {
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
@@ -1,8 +1,15 @@
import { getCategories } from './categories';
type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string;
export type DecimalCount = number | null | undefined;
interface ValueFormat {
export type ValueFormatter = (
value: number,
decimals?: DecimalCount,
scaledDecimals?: DecimalCount,
isUtc?: boolean
) => string;
export interface ValueFormat {
name: string;
id: string;
fn: ValueFormatter;
@@ -22,7 +29,7 @@ let categories: ValueFormatCategory[] = [];
const index: ValueFormatterIndex = {};
let hasBuiltIndex = false;
export function toFixed(value: number, decimals?: number): string {
export function toFixed(value: number, decimals?: DecimalCount): string {
if (value === null) {
return '';
}
@@ -50,20 +57,24 @@ export function toFixed(value: number, decimals?: number): string {
export function toFixedScaled(
value: number,
decimals: number,
scaledDecimals: number,
additionalDecimals: number,
ext: string
decimals?: DecimalCount,
scaledDecimals?: DecimalCount,
additionalDecimals?: DecimalCount,
ext?: string
) {
if (scaledDecimals === null) {
return toFixed(value, decimals) + ext;
} else {
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
if (scaledDecimals) {
if (additionalDecimals) {
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
} else {
return toFixed(value, scaledDecimals) + ext;
}
}
return toFixed(value, decimals) + ext;
}
export function toFixedUnit(unit: string) {
return (size: number, decimals: number) => {
export function toFixedUnit(unit: string): ValueFormatter {
return (size: number, decimals?: DecimalCount) => {
if (size === null) {
return '';
}
@@ -75,7 +86,7 @@ export function toFixedUnit(unit: string) {
// numeric factor. Repeatedly scales the value down by the factor until it is
// less than the factor in magnitude, or the end of the array is reached.
export function scaledUnits(factor: number, extArray: string[]) {
return (size: number, decimals: number, scaledDecimals: number) => {
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
@@ -92,7 +103,7 @@ export function scaledUnits(factor: number, extArray: string[]) {
}
}
if (steps > 0 && scaledDecimals !== null) {
if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) {
decimals = scaledDecimals + 3 * steps;
}
@@ -100,17 +111,17 @@ export function scaledUnits(factor: number, extArray: string[]) {
};
}
export function locale(value: number, decimals: number) {
export function locale(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
return value.toLocaleString(undefined, { maximumFractionDigits: decimals });
return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number });
}
export function simpleCountUnit(symbol: string) {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals: number, scaledDecimals: number) => {
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
+5 -9
View File
@@ -1,21 +1,17 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"dist",
"node_modules"
],
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["dist", "node_modules"],
"compilerOptions": {
"rootDirs": [".", "stories"],
"module": "esnext",
"outDir": "dist",
"declaration": true,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"typeRoots": ["./node_modules/@types", "types"],
"skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
},
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
Text: "Explore",
Id: "explore",
SubTitle: "Explore your data",
Icon: "fa fa-rocket",
Icon: "gicon gicon-explore",
Url: setting.AppSubUrl + "/explore",
})
}
@@ -57,6 +57,8 @@ func installCommand(c CommandLine) error {
return InstallPlugin(pluginToInstall, version, c)
}
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
// and then extracts the zip into the plugins directory.
func InstallPlugin(pluginName, version string, c CommandLine) error {
pluginFolder := c.PluginDirectory()
downloadURL := c.PluginURL()
@@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) {
return err
}
return extractFiles(body, pluginName, filePath)
}
func extractFiles(body []byte, pluginName string, filePath string) error {
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return err
@@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) {
if zf.FileInfo().IsDir() {
err := os.Mkdir(newFile, 0777)
if PermissionsError(err) {
if permissionsError(err) {
return fmt.Errorf(permissionsDeniedMessage, newFile)
}
} else {
dst, err := os.Create(newFile)
if PermissionsError(err) {
fileMode := zf.Mode()
if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") {
fileMode = os.FileMode(0755)
}
dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if permissionsError(err) {
return fmt.Errorf(permissionsDeniedMessage, newFile)
}
@@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) {
return nil
}
func PermissionsError(err error) bool {
func permissionsError(err error) bool {
return err != nil && strings.Contains(err.Error(), "permission denied")
}
@@ -1,6 +1,8 @@
package commands
import (
"io/ioutil"
"os"
"testing"
. "github.com/smartystreets/goconvey/convey"
@@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) {
})
})
}
func TestExtractFiles(t *testing.T) {
Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() {
err := os.RemoveAll("testdata/fake-plugins-dir")
So(err, ShouldBeNil)
err = os.MkdirAll("testdata/fake-plugins-dir", 0774)
So(err, ShouldBeNil)
body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
So(err, ShouldBeNil)
err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir")
So(err, ShouldBeNil)
//File in zip has permissions 777
fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64")
So(err, ShouldBeNil)
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
//File in zip has permission 664
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64")
So(err, ShouldBeNil)
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
//File in zip has permission 644
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
So(err, ShouldBeNil)
So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--")
//File in zip has permission 755
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary")
So(err, ShouldBeNil)
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
err = os.RemoveAll("testdata/fake-plugins-dir")
So(err, ShouldBeNil)
})
}
+39 -45
View File
@@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
}
}
case "diff":
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
first = points[i][0].Float64
break
}
}
// get the oldest point
points = points[0:i]
for i := 0; i < len(points); i++ {
if points[i][0].Valid {
allNull = false
value = first - points[i][0].Float64
break
}
}
allNull, value = calculateDiff(series, allNull, value, diff)
case "percent_diff":
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
first = points[i][0].Float64
break
}
}
// get the oldest point
points = points[0:i]
for i := 0; i < len(points); i++ {
if points[i][0].Valid {
allNull = false
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
value = math.Abs(val)
break
}
}
allNull, value = calculateDiff(series, allNull, value, percentDiff)
case "count_non_null":
for _, v := range series.Points {
if v[0].Valid {
@@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
func NewSimpleReducer(typ string) *SimpleReducer {
return &SimpleReducer{Type: typ}
}
func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
first = points[i][0].Float64
break
}
}
if i >= 1 {
// get the oldest point
points = points[0:i]
for i := 0; i < len(points); i++ {
if points[i][0].Valid {
allNull = false
val := fn(first, points[i][0].Float64)
value = math.Abs(val)
break
}
}
}
return allNull, value
}
var diff = func(newest, oldest float64) float64 {
return newest - oldest
}
var percentDiff = func(newest, oldest float64) float64 {
return (newest - oldest) / oldest * 100
}
@@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) {
So(result, ShouldEqual, float64(10))
})
Convey("diff with only nulls", func() {
reducer := NewSimpleReducer("diff")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
So(reducer.Reduce(series).Valid, ShouldEqual, false)
})
Convey("percent_diff one point", func() {
result := testReducer("percent_diff", 40)
So(result, ShouldEqual, float64(0))
@@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) {
result := testReducer("percent_diff", 30, 40, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
Convey("percent_diff with only nulls", func() {
reducer := NewSimpleReducer("percent_diff")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
So(reducer.Reduce(series).Valid, ShouldEqual, false)
})
})
}
+1 -1
View File
@@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
return c.dashboardRef, nil
}
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
func (c *EvalContext) GetRuleUrl() (string, error) {
if c.IsTestRun {
+2 -1
View File
@@ -3,6 +3,7 @@ package alerting
import (
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
@@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
renderOpts := rendering.Opts{
Width: 1000,
Height: 500,
Timeout: alertTimeout / 2,
Timeout: time.Duration(float64(alertTimeout) * 0.9),
OrgId: context.Rule.OrgId,
OrgRole: m.ROLE_ADMIN,
ConcurrentLimit: setting.AlertingRenderLimit,
+1
View File
@@ -33,6 +33,7 @@ func init() {
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer,
+1
View File
@@ -24,6 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
{mode: "non_negative_difference", params: []string{}, input: "max(value)", expected: `non_negative_difference(max(value))`},
}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
-1
View File
@@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
export const PANEL_HEADER_HEIGHT = 27;
export const PANEL_BORDER = 2;
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';
+3 -1
View File
@@ -27,7 +27,9 @@ export class AngularLoader {
compiledElem.remove();
},
digest: () => {
scope.$digest();
if (!scope.$$phase) {
scope.$digest();
}
},
getScope: () => {
return scope;
@@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent<Props> {
'fa-pause': rule.state !== 'paused',
});
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
return (
<li className="alert-rule-item">
@@ -21,7 +21,7 @@ exports[`Render should render component 1`] = `
className="alert-rule-item__name"
>
<a
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
>
<Highlighter
highlightClassName="highlight-search-match"
@@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
</button>
<a
className="btn btn-small btn-inverse alert-list__btn width-2"
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
title="Edit alert rule"
>
<i
@@ -248,7 +248,7 @@ export class DashNav extends PureComponent<Props> {
<div className="navbar-buttons navbar-buttons--tv">
<DashNavButton
tooltip="Cycke view mode"
tooltip="Cycle view mode"
classSuffix="tv"
icon="fa fa-desktop"
onClick={this.onToggleTVMode}
@@ -1,6 +1,6 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { DashboardPage, Props, State } from './DashboardPage';
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
import { DashboardModel } from '../state';
import { cleanUpDashboard } from '../state/actions';
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
@@ -250,4 +250,36 @@ describe('DashboardPage', () => {
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
});
});
describe('mapStateToProps with bool fullscreen', () => {
const props = mapStateToProps({
location: {
routeParams: {},
query: {
fullscreen: true,
edit: false,
},
},
dashboard: {},
} as any);
expect(props.urlFullscreen).toBe(true);
expect(props.urlEdit).toBe(false);
});
describe('mapStateToProps with string edit true', () => {
const props = mapStateToProps({
location: {
routeParams: {},
query: {
fullscreen: false,
edit: 'true',
},
},
dashboard: {},
} as any);
expect(props.urlFullscreen).toBe(false);
expect(props.urlEdit).toBe(true);
});
});
@@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent<Props, State> {
}
}
const mapStateToProps = (state: StoreState) => ({
export const mapStateToProps = (state: StoreState) => ({
urlUid: state.location.routeParams.uid,
urlSlug: state.location.routeParams.slug,
urlType: state.location.routeParams.type,
editview: state.location.query.editview,
urlPanelId: state.location.query.panelId,
urlFolderId: state.location.query.folderId,
urlFullscreen: state.location.query.fullscreen === true,
urlEdit: state.location.query.edit === true,
urlFullscreen: !!state.location.query.fullscreen,
urlEdit: !!state.location.query.edit,
initPhase: state.dashboard.initPhase,
isInitSlow: state.dashboard.isInitSlow,
initError: state.dashboard.initError,
@@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent<Props, State> {
};
renderReactPanel() {
const { dashboard, panel } = this.props;
const { dashboard, panel, isFullscreen } = this.props;
const { plugin } = this.state;
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} />;
}
renderAngularPanel() {
@@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
onMouseLeave={this.onMouseLeave}
style={styles}
>
{plugin.exports.Panel && this.renderReactPanel()}
{plugin.exports.reactPanel && this.renderReactPanel()}
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
</div>
)}
@@ -1,8 +1,6 @@
// Library
import React, { Component } from 'react';
import { Tooltip } from '@grafana/ui';
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
// Services
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
// Utils
@@ -11,6 +9,7 @@ import kbn from 'app/core/utils/kbn';
import {
DataQueryOptions,
DataQueryResponse,
DataQueryError,
LoadingState,
PanelData,
TableData,
@@ -18,8 +17,6 @@ import {
TimeSeries,
} from '@grafana/ui';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
interface RenderProps {
loading: LoadingState;
panelData: PanelData;
@@ -38,12 +35,12 @@ export interface Props {
maxDataPoints?: number;
children: (r: RenderProps) => JSX.Element;
onDataResponse?: (data: DataQueryResponse) => void;
onError: (message: string, error: DataQueryError) => void;
}
export interface State {
isFirstLoad: boolean;
loading: LoadingState;
errorMessage: string;
response: DataQueryResponse;
}
@@ -61,7 +58,6 @@ export class DataPanel extends Component<Props, State> {
this.state = {
loading: LoadingState.NotStarted,
errorMessage: '',
response: {
data: [],
},
@@ -100,6 +96,7 @@ export class DataPanel extends Component<Props, State> {
widthPixels,
maxDataPoints,
onDataResponse,
onError,
} = this.props;
if (!isVisible) {
@@ -111,7 +108,7 @@ export class DataPanel extends Component<Props, State> {
return;
}
this.setState({ loading: LoadingState.Loading, errorMessage: '' });
this.setState({ loading: LoadingState.Loading });
try {
const ds = await this.dataSourceSrv.get(datasource);
@@ -150,18 +147,22 @@ export class DataPanel extends Component<Props, State> {
isFirstLoad: false,
});
} catch (err) {
console.log('Loading error', err);
this.onError('Request Error');
}
};
console.log('DataPanel error', err);
onError = (errorMessage: string) => {
if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
this.setState({
loading: LoadingState.Error,
isFirstLoad: false,
errorMessage: errorMessage,
});
let message = 'Query error';
if (err.message) {
message = err.message;
} else if (err.data && err.data.message) {
message = err.data.message;
} else if (err.data && err.data.error) {
message = err.data.error;
} else if (err.status) {
message = `Query error: ${err.status} ${err.statusText}`;
}
onError(message, err);
this.setState({ isFirstLoad: false, loading: LoadingState.Error });
}
};
@@ -184,11 +185,11 @@ export class DataPanel extends Component<Props, State> {
render() {
const { queries } = this.props;
const { loading, isFirstLoad } = this.state;
const panelData = this.getPanelData();
if (isFirstLoad && loading === LoadingState.Loading) {
return this.renderLoadingStates();
// do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
return this.renderLoadingState();
}
if (!queries.length) {
@@ -201,46 +202,17 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{this.renderLoadingStates()}
<ErrorBoundary>
{({ error, errorInfo }) => {
if (errorInfo) {
this.onError(error.message || DEFAULT_PLUGIN_ERROR);
return null;
}
return (
<>
{this.props.children({
loading,
panelData,
})}
</>
);
}}
</ErrorBoundary>
{loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })}
</>
);
}
private renderLoadingStates(): JSX.Element {
const { loading, errorMessage } = this.state;
if (loading === LoadingState.Loading) {
return (
<div className="panel-loading">
<i className="fa fa-spinner fa-spin" />
</div>
);
} else if (loading === LoadingState.Error) {
return (
<Tooltip content={errorMessage} placement="bottom-start" theme="error">
<div className="panel-info-corner panel-info-corner--error">
<i className="fa" />
<span className="panel-info-corner-inner" />
</div>
</Tooltip>
);
}
return null;
private renderLoadingState(): JSX.Element {
return (
<div className="panel-loading">
<i className="fa fa-spinner fa-spin" />
</div>
);
}
}
@@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
// Components
import { PanelHeader } from './PanelHeader/PanelHeader';
import { DataPanel } from './DataPanel';
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
// Utils
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
@@ -17,16 +18,18 @@ import { profiler } from 'app/core/profiler';
// Types
import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types';
import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
import variables from 'sass/_variables.scss';
import templateSrv from 'app/features/templating/template_srv';
import { DataQueryResponse } from '@grafana/ui/src';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
isFullscreen: boolean;
}
export interface State {
@@ -34,6 +37,7 @@ export interface State {
renderCounter: number;
timeInfo?: string;
timeRange?: TimeRange;
errorMessage: string | null;
}
export class PanelChrome extends PureComponent<Props, State> {
@@ -45,6 +49,7 @@ export class PanelChrome extends PureComponent<Props, State> {
this.state = {
refreshCounter: 0,
renderCounter: 0,
errorMessage: null,
};
}
@@ -88,8 +93,33 @@ export class PanelChrome extends PureComponent<Props, State> {
if (this.props.dashboard.isSnapshot()) {
this.props.panel.snapshotData = dataQueryResponse.data;
}
// clear error state (if any)
this.clearErrorState();
// This event is used by old query editors and panel editor options
this.props.panel.events.emit('data-received', dataQueryResponse.data);
};
onDataError = (message: string, error: DataQueryError) => {
if (this.state.errorMessage !== message) {
this.setState({ errorMessage: message });
}
// this event is used by old query editors
this.props.panel.events.emit('data-error', error);
};
onPanelError = (message: string) => {
if (this.state.errorMessage !== message) {
this.setState({ errorMessage: message });
}
};
clearErrorState() {
if (this.state.errorMessage) {
this.setState({ errorMessage: null });
}
}
get isVisible() {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
}
@@ -110,7 +140,7 @@ export class PanelChrome extends PureComponent<Props, State> {
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.Panel;
const PanelComponent = plugin.exports.reactPanel.panel;
// This is only done to increase a counter that is used by backend
// image rendering (phantomjs/headless chrome) to know when to capture image
@@ -124,9 +154,9 @@ export class PanelChrome extends PureComponent<Props, State> {
loading={loading}
panelData={panelData}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.PanelDefaults)}
width={width - 2 * variables.panelHorizontalPadding}
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * variables.panelhorizontalpadding}
height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
renderCounter={renderCounter}
onInterpolate={this.onInterpolate}
/>
@@ -150,6 +180,7 @@ export class PanelChrome extends PureComponent<Props, State> {
widthPixels={width}
refreshCounter={refreshCounter}
onDataResponse={this.onDataResponse}
onError={this.onDataError}
>
{({ loading, panelData }) => {
return this.renderPanelPlugin(loading, panelData, width, height);
@@ -163,8 +194,8 @@ export class PanelChrome extends PureComponent<Props, State> {
};
render() {
const { dashboard, panel } = this.props;
const { timeInfo } = this.state;
const { dashboard, panel, isFullscreen } = this.props;
const { errorMessage, timeInfo } = this.state;
const { transparent } = panel;
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
@@ -185,8 +216,18 @@ export class PanelChrome extends PureComponent<Props, State> {
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={errorMessage}
isFullscreen={isFullscreen}
/>
{this.renderPanelBody(width, height)}
<ErrorBoundary>
{({ error, errorInfo }) => {
if (errorInfo) {
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
return null;
}
return this.renderPanelBody(width, height);
}}
</ErrorBoundary>
</div>
);
}}
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { isEqual } from 'lodash';
import PanelHeaderCorner from './PanelHeaderCorner';
import { PanelHeaderCorner } from './PanelHeaderCorner';
import { PanelHeaderMenu } from './PanelHeaderMenu';
import templateSrv from 'app/features/templating/template_srv';
@@ -18,6 +18,8 @@ export interface Props {
description?: string;
scopedVars?: string;
links?: [];
error?: string;
isFullscreen: boolean;
}
interface ClickCoordinates {
@@ -68,10 +70,9 @@ export class PanelHeader extends Component<Props, State> {
};
render() {
const isFullscreen = false;
const isLoading = false;
const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props;
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
const { panel, dashboard, timeInfo, scopedVars } = this.props;
const title = templateSrv.replaceWithText(panel.title, scopedVars);
return (
@@ -82,13 +83,9 @@ export class PanelHeader extends Component<Props, State> {
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={error}
/>
<div className={panelHeaderClass}>
{isLoading && (
<span className="panel-loading">
<i className="fa fa-spinner fa-spin" />
</span>
)}
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
<div className="panel-title">
<span className="icon-gf panel-alert-icon" />
@@ -6,7 +6,7 @@ import templateSrv from 'app/features/templating/template_srv';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
enum InfoModes {
enum InfoMode {
Error = 'Error',
Info = 'Info',
Links = 'Links',
@@ -18,18 +18,22 @@ interface Props {
description?: string;
scopedVars?: string;
links?: [];
error?: string;
}
export class PanelHeaderCorner extends Component<Props> {
timeSrv: TimeSrv = getTimeSrv();
getInfoMode = () => {
const { panel } = this.props;
const { panel, error } = this.props;
if (error) {
return InfoMode.Error;
}
if (!!panel.description) {
return InfoModes.Info;
return InfoMode.Info;
}
if (panel.links && panel.links.length) {
return InfoModes.Links;
return InfoMode.Links;
}
return undefined;
@@ -42,7 +46,7 @@ export class PanelHeaderCorner extends Component<Props> {
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
const html = (
return (
<div className="markdown-html">
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
{panel.links &&
@@ -62,30 +66,35 @@ export class PanelHeaderCorner extends Component<Props> {
)}
</div>
);
return html;
};
renderCornerType(infoMode: InfoMode, content: string | JSX.Element) {
const theme = infoMode === InfoMode.Error ? 'error' : 'info';
return (
<Tooltip content={content} placement="bottom-start" theme={theme}>
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
<i className="fa" />
<span className="panel-info-corner-inner" />
</div>
</Tooltip>
);
}
render() {
const infoMode: InfoModes | undefined = this.getInfoMode();
const infoMode: InfoMode | undefined = this.getInfoMode();
if (!infoMode) {
return null;
}
return (
<>
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
<Tooltip content={this.getInfoContent()} placement="bottom-start">
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
<i className="fa" />
<span className="panel-info-corner-inner" />
</div>
</Tooltip>
) : null}
</>
);
if (infoMode === InfoMode.Error) {
return this.renderCornerType(infoMode, this.props.error);
}
if (infoMode === InfoMode.Info) {
return this.renderCornerType(infoMode, this.getInfoContent());
}
return null;
}
}
export default PanelHeaderCorner;
@@ -3,7 +3,7 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
// Types
import { PanelProps } from '@grafana/ui';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
import { PanelPlugin } from 'app/types';
interface Props {
@@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
},
exports: {
Panel: NotFound,
reactPanel: new ReactPanelPlugin(NotFound),
},
};
}
@@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
render() {
return (
<EditorTabBody heading="Panel Options" toolbarItems={[]}>
<EditorTabBody heading="General" toolbarItems={[]}>
<div ref={element => (this.element = element)} />
</EditorTabBody>
);
@@ -45,7 +45,7 @@ interface PanelEditorTab {
const panelEditorTabTexts = {
[PanelEditorTabIds.Queries]: 'Queries',
[PanelEditorTabIds.Visualization]: 'Visualization',
[PanelEditorTabIds.Advanced]: 'Panel Options',
[PanelEditorTabIds.Advanced]: 'General',
[PanelEditorTabIds.Alert]: 'Alert',
};
@@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent<Props, State> {
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
<div className="flex-grow-1" />
{!isAddingMixed && (
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
Add Query
</button>
)}
@@ -28,28 +28,57 @@ interface State {
loadedDataSourceValue: string | null | undefined;
datasource: DataSourceApi | null;
isCollapsed: boolean;
angularScope: AngularQueryComponentScope | null;
hasTextEditMode: boolean;
}
export class QueryEditorRow extends PureComponent<Props, State> {
element: HTMLElement | null = null;
angularScope: AngularQueryComponentScope | null;
angularQueryEditor: AngularComponent | null = null;
state: State = {
datasource: null,
isCollapsed: false,
angularScope: null,
loadedDataSourceValue: undefined,
hasTextEditMode: false,
};
componentDidMount() {
this.loadDatasource();
this.props.panel.events.on('refresh', this.onPanelRefresh);
this.props.panel.events.on('data-error', this.onPanelDataError);
this.props.panel.events.on('data-received', this.onPanelDataReceived);
}
componentWillUnmount() {
this.props.panel.events.off('refresh', this.onPanelRefresh);
this.props.panel.events.off('data-error', this.onPanelDataError);
this.props.panel.events.off('data-received', this.onPanelDataReceived);
if (this.angularQueryEditor) {
this.angularQueryEditor.destroy();
}
}
onPanelDataError = () => {
// Some query controllers listen to data error events and need a digest
if (this.angularQueryEditor) {
// for some reason this needs to be done in next tick
setTimeout(this.angularQueryEditor.digest);
}
};
onPanelDataReceived = () => {
// Some query controllers listen to data error events and need a digest
if (this.angularQueryEditor) {
// for some reason this needs to be done in next tick
setTimeout(this.angularQueryEditor.digest);
}
};
onPanelRefresh = () => {
if (this.state.angularScope) {
this.state.angularScope.range = getTimeSrv().timeRange();
if (this.angularScope) {
this.angularScope.range = getTimeSrv().timeRange();
}
};
@@ -73,7 +102,11 @@ export class QueryEditorRow extends PureComponent<Props, State> {
const dataSourceSrv = getDatasourceSrv();
const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
this.setState({ datasource, loadedDataSourceValue: this.props.dataSourceValue });
this.setState({
datasource,
loadedDataSourceValue: this.props.dataSourceValue,
hasTextEditMode: false,
});
}
componentDidUpdate() {
@@ -98,21 +131,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
this.angularQueryEditor = loader.load(this.element, scopeProps, template);
this.angularScope = scopeProps.ctrl;
// give angular time to compile
setTimeout(() => {
this.setState({ angularScope: scopeProps.ctrl });
this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode });
}, 10);
}
componentWillUnmount() {
this.props.panel.events.off('refresh', this.onPanelRefresh);
if (this.angularQueryEditor) {
this.angularQueryEditor.destroy();
}
}
onToggleCollapse = () => {
this.setState({ isCollapsed: !this.state.isCollapsed });
};
@@ -138,10 +164,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}
onToggleEditMode = () => {
const { angularScope } = this.state;
if (angularScope && angularScope.toggleEditorMode) {
angularScope.toggleEditorMode();
if (this.angularScope && this.angularScope.toggleEditorMode) {
this.angularScope.toggleEditorMode();
this.angularQueryEditor.digest();
}
@@ -150,11 +174,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}
};
get hasTextEditMode() {
const { angularScope } = this.state;
return angularScope && angularScope.toggleEditorMode;
}
onRemoveQuery = () => {
this.props.onRemoveQuery(this.props.query);
};
@@ -171,10 +190,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
};
renderCollapsedText(): string | null {
const { angularScope } = this.state;
if (angularScope && angularScope.getCollapsedText) {
return angularScope.getCollapsedText();
if (this.angularScope && this.angularScope.getCollapsedText) {
return this.angularScope.getCollapsedText();
}
return null;
@@ -182,7 +199,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
render() {
const { query, inMixedMode } = this.props;
const { datasource, isCollapsed } = this.state;
const { datasource, isCollapsed, hasTextEditMode } = this.state;
const isDisabled = query.hide;
const bodyClasses = classNames('query-editor-row__body gf-form-query', {
@@ -212,7 +229,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
</div>
<div className="query-editor-row__actions">
{this.hasTextEditMode && (
{hasTextEditMode && (
<button
className="query-editor-row__action"
onClick={this.onToggleEditMode}
@@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent<Props, State> {
};
}
getPanelDefaultOptions = () => {
getReactPanelOptions = () => {
const { panel, plugin } = this.props;
if (plugin.exports.PanelDefaults) {
return panel.getOptions(plugin.exports.PanelDefaults.options);
}
return panel.getOptions(plugin.exports.PanelDefaults);
return panel.getOptions(plugin.exports.reactPanel.defaults);
};
renderPanelOptions() {
const { plugin, angularPanel } = this.props;
const { PanelOptions } = plugin.exports;
if (angularPanel) {
return <div ref={element => (this.element = element)} />;
}
return (
<>
{PanelOptions ? (
<PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
) : (
<p>Visualization has no options</p>
)}
</>
);
if (plugin.exports.reactPanel) {
const PanelEditor = plugin.exports.reactPanel.editor;
if (PanelEditor) {
return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
}
}
return <p>Visualization has no options</p>;
}
componentDidMount() {
@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(17);
expect(model.schemaVersion).toBe(18);
});
it('graph thresholds should be migrated', () => {
@@ -22,7 +22,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades = [];
this.dashboard.schemaVersion = 17;
this.dashboard.schemaVersion = 18;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@@ -387,6 +387,36 @@ export class DashboardMigrator {
});
}
if (oldVersion < 18) {
// migrate change to gauge options
panelUpgrades.push(panel => {
if (panel['options-gauge']) {
panel.options = panel['options-gauge'];
panel.options.valueOptions = {
unit: panel.options.unit,
stat: panel.options.stat,
decimals: panel.options.decimals,
prefix: panel.options.prefix,
suffix: panel.options.suffix,
};
// correct order
if (panel.options.thresholds) {
panel.options.thresholds.reverse();
}
// this options prop was due to a bug
delete panel.options.options;
delete panel.options.unit;
delete panel.options.stat;
delete panel.options.decimals;
delete panel.options.prefix;
delete panel.options.suffix;
delete panel['options-gauge'];
}
});
}
if (panelUpgrades.length === 0) {
return;
}
@@ -10,6 +10,20 @@ describe('PanelModel', () => {
type: 'table',
showColumns: true,
targets: [{ refId: 'A' }, { noRefId: true }],
options: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
});
});
@@ -35,6 +49,21 @@ describe('PanelModel', () => {
expect(saveModel.events).toBe(undefined);
});
it('should restore -Infinity value for base threshold', () => {
expect(model.options.thresholds).toEqual([
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: -Infinity,
},
]);
});
describe('when changing panel type', () => {
beforeEach(() => {
model.changeType('graph', true);
@@ -55,5 +84,19 @@ describe('PanelModel', () => {
expect(model.alert).toBe(undefined);
});
});
describe('get panel options', () => {
it('should apply defaults', () => {
model.options = { existingProp: 10 };
const options = model.getOptions({
defaultProp: true,
existingProp: 0,
});
expect(options.defaultProp).toBe(true);
expect(options.existingProp).toBe(10);
expect(model.options).toBe(options);
});
});
});
});
@@ -3,8 +3,7 @@ import _ from 'lodash';
// Types
import { Emitter } from 'app/core/utils/emitter';
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
import { DataQuery, TimeSeries } from '@grafana/ui';
import { DataQuery, TimeSeries, Threshold } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
export interface GridPos {
@@ -47,8 +46,6 @@ const mustKeepProps: { [str: string]: boolean } = {
timeFrom: true,
timeShift: true,
hideTimeOverride: true,
maxDataPoints: true,
interval: true,
description: true,
links: true,
fullscreen: true,
@@ -92,6 +89,9 @@ export class PanelModel {
timeFrom?: any;
timeShift?: any;
hideTimeOverride?: any;
options: {
[key: string]: any;
};
maxDataPoints?: number;
interval?: string;
@@ -105,8 +105,6 @@ export class PanelModel {
hasRefreshed: boolean;
events: Emitter;
cacheTimeout?: any;
// cache props between plugins
cachedPluginOptions?: any;
constructor(model) {
@@ -121,6 +119,8 @@ export class PanelModel {
_.defaultsDeep(this, _.cloneDeep(defaults));
// queries must have refId
this.ensureQueryIds();
this.restoreInfintyForThresholds();
}
ensureQueryIds() {
@@ -133,21 +133,28 @@ export class PanelModel {
}
}
restoreInfintyForThresholds() {
if (this.options && this.options.thresholds) {
this.options.thresholds = this.options.thresholds.map((threshold: Threshold) => {
// JSON serialization of -Infinity is 'null' so lets convert it back to -Infinity
if (threshold.index === 0 && threshold.value === null) {
return { ...threshold, value: -Infinity };
}
return threshold;
});
}
}
getOptions(panelDefaults) {
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
return _.defaultsDeep(this.options || {}, panelDefaults);
}
updateOptions(options: object) {
const update: any = {};
update[this.getOptionsKey()] = options;
Object.assign(this, update);
this.options = options;
this.render();
}
private getOptionsKey() {
return PANEL_OPTIONS_KEY_PREFIX + this.type;
}
getSaveModel() {
const model: any = {};
for (const property in this) {
@@ -240,14 +247,15 @@ export class PanelModel {
// for angular panels only we need to remove all events and let angular panels do some cleanup
if (fromAngularPanel) {
this.destroy();
}
for (const key of _.keys(this)) {
if (mustKeepProps[key]) {
continue;
}
delete this[key];
// remove panel type specific options
for (const key of _.keys(this)) {
if (mustKeepProps[key]) {
continue;
}
delete this[key];
}
this.restorePanelOptions(pluginId);
@@ -103,18 +103,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
<div className="explore-toolbar-header-title">
{exploreId === 'left' && (
<span className="navbar-page-btn">
<i className="fa fa-rocket fa-fw" />
<i className="gicon gicon-explore" />
Explore
</span>
)}
</div>
<div className="explore-toolbar-header-close">
{exploreId === 'right' && (
<a onClick={this.props.closeSplit}>
<i className="fa fa-times fa-fw" />
</a>
)}
</div>
{exploreId === 'right' && (
<a className="explore-toolbar-header-close" onClick={this.props.closeSplit}>
<i className="fa fa-times fa-fw" />
</a>
)}
</div>
</div>
<div className="explore-toolbar-item">
@@ -156,7 +154,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
splitted,
title: 'Run Query',
onClick: this.onRunQuery,
buttonClassName: 'navbar-button--primary',
buttonClassName: 'navbar-button--secondary',
iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
iconSide: IconSide.right,
})}
@@ -1,9 +1,14 @@
// Libraries
import React, { PureComponent } from 'react';
import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui';
// Components
import { Switch, PanelOptionsGroup } from '@grafana/ui';
// Types
import { FormField, PanelEditorProps } from '@grafana/ui';
import { GaugeOptions } from './types';
export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
onToggleThresholdLabels = () =>
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
+12 -4
View File
@@ -16,9 +16,10 @@ interface Props extends PanelProps<GaugeOptions> {}
export class GaugePanel extends PureComponent<Props> {
render() {
const { panelData, width, height, onInterpolate, options } = this.props;
const { valueOptions } = options;
const prefix = onInterpolate(options.prefix);
const suffix = onInterpolate(options.suffix);
const prefix = onInterpolate(valueOptions.prefix);
const suffix = onInterpolate(valueOptions.suffix);
let value: TimeSeriesValue;
if (panelData.timeSeries) {
@@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent<Props> {
});
if (vmSeries[0]) {
value = vmSeries[0].stats[options.stat];
value = vmSeries[0].stats[valueOptions.stat];
} else {
value = null;
}
@@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent<Props> {
{theme => (
<Gauge
value={value}
{...this.props.options}
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}
/>
)}
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import {
PanelOptionsProps,
PanelEditorProps,
ThresholdsEditor,
Threshold,
PanelOptionsGrid,
@@ -8,29 +8,11 @@ import {
ValueMapping,
} from '@grafana/ui';
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
import GaugeOptionsEditor from './GaugeOptionsEditor';
import { GaugeOptions } from './types';
export const defaultProps = {
options: {
minValue: 0,
maxValue: 100,
prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
suffix: '',
decimals: 0,
stat: 'avg',
unit: 'none',
valueMappings: [],
thresholds: [],
},
};
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
static defaultProps = defaultProps;
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { GaugeOptionsBox } from './GaugeOptionsBox';
import { GaugeOptions, SingleStatValueOptions } from './types';
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) =>
this.props.onChange({
...this.props.options,
@@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
valueMappings,
});
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
this.props.onChange({
...this.props.options,
valueOptions,
});
render() {
const { onChange, options } = this.props;
return (
<>
<PanelOptionsGrid>
<ValueOptions onChange={onChange} options={options} />
<GaugeOptionsEditor onChange={onChange} options={options} />
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
<GaugeOptionsBox onChange={onChange} options={options} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid>
@@ -1,7 +1,12 @@
// Libraries
import React, { PureComponent } from 'react';
import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
// Components
import UnitPicker from 'app/core/components/Select/UnitPicker';
import { GaugeOptions } from './types';
import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
// Types
import { SingleStatValueOptions } from './types';
const statOptions = [
{ value: 'min', label: 'Min' },
@@ -19,24 +24,40 @@ const statOptions = [
const labelWidth = 6;
export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
export interface Props {
options: SingleStatValueOptions;
onChange: (valueOptions: SingleStatValueOptions) => void;
}
export class SingleStatValueEditor extends PureComponent<Props> {
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
onDecimalChange = event => {
if (!isNaN(event.target.value)) {
this.props.onChange({ ...this.props.options, decimals: event.target.value });
this.props.onChange({
...this.props.options,
decimals: parseInt(event.target.value, 10),
});
} else {
this.props.onChange({
...this.props.options,
decimals: null,
});
}
};
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
render() {
const { stat, unit, decimals, prefix, suffix } = this.props.options;
let decimalsString = '';
if (Number.isFinite(decimals)) {
decimalsString = decimals.toString();
}
return (
<PanelOptionsGroup title="Value">
<div className="gf-form">
@@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
labelWidth={labelWidth}
placeholder="auto"
onChange={this.onDecimalChange}
value={decimals || ''}
value={decimalsString}
type="number"
/>
<FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
+9 -3
View File
@@ -1,4 +1,10 @@
import GaugePanelOptions, { defaultProps } from './GaugePanelOptions';
import { GaugePanel } from './GaugePanel';
import { ReactPanelPlugin } from '@grafana/ui';
export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults };
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
reactPanel.setEditor(GaugePanelEditor);
reactPanel.setDefaults(defaults);
+25 -5
View File
@@ -1,15 +1,35 @@
import { Threshold, ValueMapping } from '@grafana/ui';
export interface GaugeOptions {
decimals: number;
valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
prefix: string;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
stat: string;
suffix: string;
thresholds: Threshold[];
unit: string;
valueOptions: SingleStatValueOptions;
}
export interface SingleStatValueOptions {
unit: string;
suffix: string;
stat: string;
prefix: string;
decimals?: number | null;
}
export const defaults: GaugeOptions = {
minValue: 0,
maxValue: 100,
showThresholdMarkers: true,
showThresholdLabels: false,
valueOptions: {
prefix: '',
suffix: '',
decimals: null,
stat: 'avg',
unit: 'none',
},
valueMappings: [],
thresholds: [],
};
@@ -3,10 +3,10 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
// Types
import { PanelOptionsProps, Switch } from '@grafana/ui';
import { PanelEditorProps, Switch } from '@grafana/ui';
import { Options } from './types';
export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {
export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
onToggleLines = () => {
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
};
+2 -2
View File
@@ -1,4 +1,4 @@
import { GraphPanel } from './GraphPanel';
import { GraphPanelOptions } from './GraphPanelOptions';
import { GraphPanelEditor } from './GraphPanelEditor';
export { GraphPanel as Panel, GraphPanelOptions as PanelOptions };
export { GraphPanel as Panel, GraphPanelEditor as PanelOptions };
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { PanelProps } from '@grafana/ui';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
export class Text2 extends PureComponent<PanelProps> {
constructor(props: PanelProps) {
@@ -11,4 +11,4 @@ export class Text2 extends PureComponent<PanelProps> {
}
}
export { Text2 as Panel };
export const reactPanel = new ReactPanelPlugin(Text2);
@@ -0,0 +1,24 @@
<?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="22px" height="22px" viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#E3E3E3;}
</style>
<g>
<polygon class="st0" points="7.93,14.21 6.99,17.47 9.52,15.21 9,14.28 "/>
<polygon class="st0" points="14.15,7.86 15.08,4.6 12.56,6.86 13.08,7.79 "/>
<polygon class="st0" points="14.28,13.07 14.22,14.14 17.47,15.08 15.22,12.55 "/>
<polygon class="st0" points="7.8,9 7.86,7.93 4.61,6.99 6.86,9.52 "/>
<path class="st0" d="M8.82,1.31L8.36,9.35l-7.05,3.9l8.04,0.46l3.9,7.05l0.46-8.04l7.05-3.9l-8.04-0.46L8.82,1.31z M11.38,11.78
c-0.41,0.19-0.89,0.01-1.08-0.4c-0.19-0.41-0.01-0.89,0.4-1.08c0.41-0.19,0.89-0.01,1.08,0.4C11.97,11.1,11.79,11.59,11.38,11.78z"
/>
<path class="st1" d="M21.72,8.56c-1.35-5.92-7.24-9.63-13.16-8.28C2.64,1.63-1.07,7.52,0.28,13.44s7.24,9.63,13.16,8.28
C19.36,20.37,23.07,14.48,21.72,8.56z M15.08,4.6l-0.94,3.25l-1.07-0.06l-0.52-0.94L15.08,4.6z M7.86,7.93L7.8,9L6.86,9.52
L4.61,6.99L7.86,7.93z M6.99,17.47l0.94-3.25L9,14.28l0.52,0.94L6.99,17.47z M14.22,14.14l0.06-1.07l0.94-0.52l2.25,2.53
L14.22,14.14z M13.72,12.72l-0.46,8.04l-3.9-7.05l-8.04-0.46l7.05-3.9l0.46-8.04l3.9,7.05l8.04,0.46L13.72,12.72z"/>
<path class="st1" d="M10.7,10.29c-0.41,0.19-0.59,0.67-0.4,1.08c0.19,0.41,0.67,0.59,1.08,0.4c0.41-0.19,0.59-0.67,0.4-1.08
C11.59,10.28,11.1,10.11,10.7,10.29z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,24 @@
<?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="22px" height="22px" viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#52545c;}
</style>
<g>
<polygon class="st0" points="7.93,14.21 6.99,17.47 9.52,15.21 9,14.28 "/>
<polygon class="st0" points="14.15,7.86 15.08,4.6 12.56,6.86 13.08,7.79 "/>
<polygon class="st0" points="14.28,13.07 14.22,14.14 17.47,15.08 15.22,12.55 "/>
<polygon class="st0" points="7.8,9 7.86,7.93 4.61,6.99 6.86,9.52 "/>
<path class="st0" d="M8.82,1.31L8.36,9.35l-7.05,3.9l8.04,0.46l3.9,7.05l0.46-8.04l7.05-3.9l-8.04-0.46L8.82,1.31z M11.38,11.78
c-0.41,0.19-0.89,0.01-1.08-0.4c-0.19-0.41-0.01-0.89,0.4-1.08c0.41-0.19,0.89-0.01,1.08,0.4C11.97,11.1,11.79,11.59,11.38,11.78z"
/>
<path class="st1" d="M21.72,8.56c-1.35-5.92-7.24-9.63-13.16-8.28C2.64,1.63-1.07,7.52,0.28,13.44s7.24,9.63,13.16,8.28
C19.36,20.37,23.07,14.48,21.72,8.56z M15.08,4.6l-0.94,3.25l-1.07-0.06l-0.52-0.94L15.08,4.6z M7.86,7.93L7.8,9L6.86,9.52
L4.61,6.99L7.86,7.93z M6.99,17.47l0.94-3.25L9,14.28l0.52,0.94L6.99,17.47z M14.22,14.14l0.06-1.07l0.94-0.52l2.25,2.53
L14.22,14.14z M13.72,12.72l-0.46,8.04l-3.9-7.05l-8.04-0.46l7.05-3.9l0.46-8.04l3.9,7.05l8.04,0.46L13.72,12.72z"/>
<path class="st1" d="M10.7,10.29c-0.41,0.19-0.59,0.67-0.4,1.08c0.19,0.41,0.67,0.59,1.08,0.4c0.41-0.19,0.59-0.67,0.4-1.08
C11.59,10.28,11.1,10.11,10.7,10.29z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
export interface GrafanaVariables {
panelHorizontalPadding: number;
panelVerticalPadding: number;
panelhorizontalpadding: number;
panelverticalpadding: number;
}
declare const variables: GrafanaVariables;
+7
View File
@@ -192,6 +192,10 @@
background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg');
}
.gicon-explore {
background-image: url('../img/icons_#{$theme-name}_theme/icon_explore.svg');
}
.sidemenu {
.gicon-dashboard {
background-image: url('../img/icons_dark_theme/icon_dashboard.svg');
@@ -205,6 +209,9 @@
.gicon-question {
background-image: url('../img/icons_dark_theme/icon_question.svg');
}
.gicon-explore {
background-image: url('../img/icons_dark_theme/icon_explore.svg');
}
}
.fa--permissions-list {
+14 -17
View File
@@ -1,9 +1,8 @@
.navbar {
position: relative;
padding-left: 20px;
z-index: $zindex-navbar-fixed;
height: $navbarHeight;
padding-right: 20px;
padding: 0 20px 0 50px;
display: flex;
flex-grow: 1;
border-bottom: 1px solid transparent;
@@ -46,9 +45,8 @@
.navbar-button--add-panel,
.navbar-button--star,
.navbar-button--tv,
.navbar-buttons--close {
display: flex;
.navbar-button--tv {
display: none;
}
}
@@ -58,15 +56,14 @@
white-space: nowrap;
display: block;
margin: 0;
color: darken($link-color, 5%);
color: $headings-color;
font-size: $font-size-lg;
padding-left: 1rem;
min-height: $navbarHeight;
line-height: $navbarHeight;
.fa-caret-down {
font-size: 60%;
padding-left: 0.2rem;
padding-left: 6px;
}
&--search {
@@ -74,12 +71,12 @@
}
.gicon {
top: -2px;
position: relative;
top: -1px;
font-size: 19px;
font-size: 17px;
line-height: 8px;
opacity: 0.75;
margin-right: 13px;
margin-right: 10px;
display: none;
}
@@ -124,7 +121,7 @@
height: 30px;
color: $text-muted;
border: 1px solid $navbar-button-border;
margin-right: 3px;
margin-left: 3px;
white-space: nowrap;
.gicon {
@@ -153,19 +150,19 @@
}
}
&--primary {
@include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
&--secondary {
@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl);
}
}
@include media-breakpoint-up(sm) {
.navbar {
padding-left: 50px;
padding-left: 60px;
}
.sidemenu-open {
.navbar {
padding-left: 15px;
padding-left: 25px;
margin-left: 0;
}
}
@@ -181,7 +178,7 @@
display: flex;
height: $navbarHeight;
align-items: center;
padding-left: 7px;
padding-right: 13px;
}
.navbar-edit__back-btn {
+1 -1
View File
@@ -253,7 +253,7 @@ li.sidemenu-org-switcher {
}
.sidemenu__logo_small_breakpoint {
padding: 16px 10px 26px;
padding: 14px 10px 26px 13px;
display: flex;
flex-direction: row;
justify-content: space-between;
+2 -25
View File
@@ -30,12 +30,6 @@
padding: 0;
margin-left: 0;
}
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
}
}
}
.explore-toolbar {
@@ -44,7 +38,7 @@
flex-flow: row wrap;
justify-content: flex-start;
height: auto;
padding: 0px $dashboard-padding;
padding: 0px $dashboard-padding 0 25px;
border-bottom: 1px solid #0000;
transition-duration: 0.35s;
transition-timing-function: ease-in-out;
@@ -87,22 +81,9 @@
align-items: center;
}
.explore-toolbar-header-title {
color: darken($link-color, 5%);
.navbar-page-btn {
padding-left: $dashboard-padding;
}
.fa {
font-size: 100%;
opacity: 0.75;
margin-right: 0.5em;
}
}
.explore-toolbar-header-close {
margin-left: auto;
color: $text-color-weak;
}
.explore-toolbar-content {
@@ -156,7 +137,6 @@
.sidemenu-open {
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: 0;
}
}
@@ -164,7 +144,6 @@
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}
@@ -185,7 +164,6 @@
.sidemenu-open {
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}
@@ -193,7 +171,6 @@
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}