mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 13:14:35 +08:00
Compare commits
40 Commits
docs/add-d
...
v6.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a9a621b6 | ||
|
|
0655a23091 | ||
|
|
0f63051a3b | ||
|
|
4a2852d0ed | ||
|
|
682baf8d46 | ||
|
|
9c2adc115c | ||
|
|
f93cd857cb | ||
|
|
28d5737221 | ||
|
|
d85a4c3dd0 | ||
|
|
115215d317 | ||
|
|
99fecdcf46 | ||
|
|
9bfcfe271f | ||
|
|
251596aed4 | ||
|
|
daa6819e0c | ||
|
|
19192fc0b5 | ||
|
|
57977c9db6 | ||
|
|
82cf6b951a | ||
|
|
56f75e9e9d | ||
|
|
a32d5ed16a | ||
|
|
84bd280c7a | ||
|
|
1ce7ef7ae0 | ||
|
|
9d5529c453 | ||
|
|
0fd59e72b0 | ||
|
|
6fc3c6a7ed | ||
|
|
5091e06e4c | ||
|
|
b229b16259 | ||
|
|
d45fc0cadd | ||
|
|
3a36c750dd | ||
|
|
6da26aa3cd | ||
|
|
652ea3c08e | ||
|
|
acb329f2f1 | ||
|
|
d757c74442 | ||
|
|
688a8e286d | ||
|
|
d924d3f6c9 | ||
|
|
754be5a66e | ||
|
|
9e457077c6 | ||
|
|
c3e5a2c968 | ||
|
|
dcb3a344d2 | ||
|
|
1a2b5cd3cc | ||
|
|
6a71b199ca |
@@ -5,7 +5,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "6.0.0-pre3",
|
"version": "6.0.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"postcss-browser-reporter": "^0.5.0",
|
"postcss-browser-reporter": "^0.5.0",
|
||||||
"postcss-loader": "^2.0.6",
|
"postcss-loader": "^2.0.6",
|
||||||
"postcss-reporter": "^5.0.0",
|
"postcss-reporter": "^5.0.0",
|
||||||
"prettier": "1.9.2",
|
"prettier": "1.16.4",
|
||||||
"react-hot-loader": "^4.3.6",
|
"react-hot-loader": "^4.3.6",
|
||||||
"react-test-renderer": "^16.5.0",
|
"react-test-renderer": "^16.5.0",
|
||||||
"redux-mock-store": "^1.5.3",
|
"redux-mock-store": "^1.5.3",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"jest": "jest --notify --watch",
|
"jest": "jest --notify --watch",
|
||||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||||
"storybook": "cd packages/grafana-ui && yarn storybook",
|
"storybook": "cd packages/grafana-ui && yarn storybook",
|
||||||
"prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
|
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\""
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Component, createRef } from 'react';
|
import React, { Component, createRef } from 'react';
|
||||||
import PopperController from '../Tooltip/PopperController';
|
import { PopperController } from '../Tooltip/PopperController';
|
||||||
import Popper from '../Tooltip/Popper';
|
import { Popper } from '../Tooltip/Popper';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { Themeable } from '../../types';
|
import { Themeable } from '../../types';
|
||||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsP
|
|||||||
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
|
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
|
||||||
import { PopperContentProps } from '../Tooltip/PopperController';
|
import { PopperContentProps } from '../Tooltip/PopperController';
|
||||||
import SpectrumPalette from './SpectrumPalette';
|
import SpectrumPalette from './SpectrumPalette';
|
||||||
import { GrafanaThemeType } from '@grafana/ui';
|
import { GrafanaThemeType } from '../../types/theme';
|
||||||
|
|
||||||
export interface Props<T> extends ColorPickerProps, PopperContentProps {
|
export interface Props<T> extends ColorPickerProps, PopperContentProps {
|
||||||
customPickers?: T;
|
customPickers?: T;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { DeleteButton } from '@grafana/ui';
|
import { DeleteButton } from './DeleteButton';
|
||||||
|
|
||||||
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
|
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.empty-search-result {
|
||||||
|
border-left: 3px solid $info-box-border-color;
|
||||||
|
background-color: $empty-list-cta-bg;
|
||||||
|
padding: $spacer;
|
||||||
|
min-width: 350px;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
margin-bottom: $spacer * 2;
|
||||||
|
}
|
||||||
@@ -84,9 +84,9 @@ describe('Get thresholds formatted', () => {
|
|||||||
it('should get the correct formatted values when thresholds are added', () => {
|
it('should get the correct formatted values when thresholds are added', () => {
|
||||||
const { instance } = setup({
|
const { instance } = setup({
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ 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;
|
type TimeSeriesValue = string | number | null;
|
||||||
|
|
||||||
export interface Props extends Themeable {
|
export interface Props extends Themeable {
|
||||||
decimals: number;
|
decimals?: number | null;
|
||||||
height: number;
|
height: number;
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
@@ -98,16 +98,15 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
getFormattedThresholds() {
|
getFormattedThresholds() {
|
||||||
const { maxValue, minValue, thresholds, theme } = this.props;
|
const { maxValue, minValue, thresholds, theme } = this.props;
|
||||||
|
|
||||||
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
|
const lastThreshold = thresholds[thresholds.length - 1];
|
||||||
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...thresholdsSortedByIndex.map(threshold => {
|
...thresholds.map(threshold => {
|
||||||
if (threshold.index === 0) {
|
if (threshold.index === 0) {
|
||||||
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
|
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) };
|
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
|
||||||
}),
|
}),
|
||||||
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
|
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
|
||||||
@@ -116,9 +115,9 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
|
|
||||||
getFontScale(length: number): number {
|
getFontScale(length: number): number {
|
||||||
if (length > 12) {
|
if (length > 12) {
|
||||||
return FONT_SCALE - length * 5 / 120;
|
return FONT_SCALE - (length * 5) / 110;
|
||||||
}
|
}
|
||||||
return FONT_SCALE - length * 5 / 105;
|
return FONT_SCALE - (length * 5) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
|
|||||||
showBars: false,
|
showBars: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
element: HTMLElement | null;
|
element: HTMLElement | null = null;
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: Partial<Props>) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
@@ -11,12 +10,26 @@ const setup = (propOverrides?: object) => {
|
|||||||
|
|
||||||
Object.assign(props, propOverrides);
|
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', () => {
|
describe('Initialization', () => {
|
||||||
it('should add a base threshold if missing', () => {
|
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' }]);
|
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||||
});
|
});
|
||||||
@@ -24,7 +37,7 @@ describe('Initialization', () => {
|
|||||||
|
|
||||||
describe('Add threshold', () => {
|
describe('Add threshold', () => {
|
||||||
it('should not add threshold at index 0', () => {
|
it('should not add threshold at index 0', () => {
|
||||||
const instance = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.onAddThreshold(0);
|
instance.onAddThreshold(0);
|
||||||
|
|
||||||
@@ -32,32 +45,32 @@ describe('Add threshold', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add threshold', () => {
|
it('should add threshold', () => {
|
||||||
const instance = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.onAddThreshold(1);
|
instance.onAddThreshold(1);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add another threshold above a first', () => {
|
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' }],
|
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.onAddThreshold(2);
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ 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', () => {
|
it('should add another threshold between first and second index', () => {
|
||||||
const instance = setup({
|
const { instance } = setup({
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
@@ -68,10 +81,10 @@ describe('Add threshold', () => {
|
|||||||
instance.onAddThreshold(2);
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
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: 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: 1, value: 50, color: '#EAB839' },
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
];
|
];
|
||||||
const instance = setup({ thresholds });
|
const { instance } = setup({ thresholds });
|
||||||
|
|
||||||
instance.onRemoveThreshold(thresholds[0]);
|
instance.onRemoveThreshold(thresholds[0]);
|
||||||
|
|
||||||
@@ -96,9 +109,7 @@ describe('Remove threshold', () => {
|
|||||||
{ index: 1, value: 50, color: '#EAB839' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
];
|
];
|
||||||
const instance = setup({
|
const { instance } = setup({ thresholds });
|
||||||
thresholds,
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.onRemoveThreshold(thresholds[1]);
|
instance.onRemoveThreshold(thresholds[1]);
|
||||||
|
|
||||||
@@ -116,7 +127,7 @@ describe('change threshold value', () => {
|
|||||||
{ index: 1, value: 50, color: '#EAB839' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
];
|
];
|
||||||
const instance = setup({ thresholds });
|
const { instance } = setup({ thresholds });
|
||||||
|
|
||||||
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
|
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
@@ -126,7 +137,7 @@ describe('change threshold value', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update value', () => {
|
it('should update value', () => {
|
||||||
const instance = setup();
|
const { instance } = setup();
|
||||||
const thresholds = [
|
const thresholds = [
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
@@ -150,24 +161,24 @@ describe('change threshold value', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('on blur threshold value', () => {
|
describe('on blur threshold value', () => {
|
||||||
it('should resort rows and update indexes', () => {
|
it.only('should resort rows and update indexes', () => {
|
||||||
const instance = setup();
|
const { instance } = setup();
|
||||||
const thresholds = [
|
const thresholds = [
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
{ index: 1, value: 78, color: '#EAB839' },
|
{ index: 1, value: 78, color: '#EAB839' },
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
];
|
];
|
||||||
|
|
||||||
instance.state = {
|
instance.setState({
|
||||||
thresholds,
|
thresholds,
|
||||||
};
|
});
|
||||||
|
|
||||||
instance.onBlur();
|
instance.onBlur();
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 2, value: 78, color: '#EAB839' },
|
|
||||||
{ index: 1, value: 75, color: '#6ED0E0' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 2, value: 78, color: '#EAB839' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { PureComponent, ChangeEvent } from 'react';
|
import React, { PureComponent, ChangeEvent } from 'react';
|
||||||
import { Threshold } from '../../types';
|
import { Threshold } from '../../types';
|
||||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
import { ColorPicker } from '..';
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
import { PanelOptionsGroup } from '..';
|
||||||
import { colors } from '../../utils';
|
import { colors } from '../../utils';
|
||||||
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
|
import { ThemeContext } from '../../themes/ThemeContext';
|
||||||
|
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
@@ -54,16 +55,16 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
||||||
|
|
||||||
// Set a color
|
// 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))[1];
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
thresholds: this.sortThresholds([
|
thresholds: this.sortThresholds([
|
||||||
...newThresholds,
|
...newThresholds,
|
||||||
{
|
{
|
||||||
|
color,
|
||||||
index,
|
index,
|
||||||
value: value as number,
|
value: value as number,
|
||||||
color,
|
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@@ -137,10 +138,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||||
let index = sortThresholds.length - 1;
|
let index = 0;
|
||||||
sortThresholds.forEach(t => {
|
sortThresholds.forEach(t => {
|
||||||
t.index = index--;
|
t.index = index++;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { thresholds: sortThresholds };
|
return { thresholds: sortThresholds };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,12 +155,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
sortThresholds = (thresholds: Threshold[]) => {
|
sortThresholds = (thresholds: Threshold[]) => {
|
||||||
return thresholds.sort((t1, t2) => {
|
return thresholds.sort((t1, t2) => {
|
||||||
return t2.value - t1.value;
|
return t1.value - t2.value;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderInput = (threshold: Threshold) => {
|
renderInput = (threshold: Threshold) => {
|
||||||
const value = threshold.index === 0 ? 'Base' : threshold.value;
|
|
||||||
return (
|
return (
|
||||||
<div className="thresholds-row-input-inner">
|
<div className="thresholds-row-input-inner">
|
||||||
<span className="thresholds-row-input-inner-arrow" />
|
<span className="thresholds-row-input-inner-arrow" />
|
||||||
@@ -169,51 +170,60 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="thresholds-row-input-inner-value">
|
{threshold.index === 0 && (
|
||||||
<input
|
<div className="thresholds-row-input-inner-value">
|
||||||
type="number"
|
<input type="text" value="Base" readOnly />
|
||||||
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" />
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { thresholds } = this.state;
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Consumer>
|
<ThemeContext.Consumer>
|
||||||
{theme => {
|
{theme => {
|
||||||
return (
|
return (
|
||||||
<PanelOptionsGroup title="Thresholds">
|
<PanelOptionsGroup title="Thresholds">
|
||||||
<div className="thresholds">
|
<div className="thresholds">
|
||||||
{thresholds.map((threshold, index) => {
|
{thresholds
|
||||||
return (
|
.slice(0)
|
||||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
.reverse()
|
||||||
<div
|
.map((threshold, index) => {
|
||||||
className="thresholds-row-add-button"
|
return (
|
||||||
onClick={() => this.onAddThreshold(threshold.index + 1)}
|
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||||
>
|
<div
|
||||||
<i className="fa fa-plus" />
|
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>
|
||||||
<div
|
);
|
||||||
className="thresholds-row-color-indicator"
|
})}
|
||||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
|
||||||
/>
|
|
||||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</PanelOptionsGroup>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-row-input {
|
.thresholds-row-input {
|
||||||
margin-top: 49px;
|
margin-top: 44px;
|
||||||
margin-left: 2px;
|
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>
|
||||||
|
`;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import * as PopperJS from 'popper.js';
|
import * as PopperJS from 'popper.js';
|
||||||
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
|
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
|
||||||
import { Portal } from '@grafana/ui';
|
import { Portal } from '../Portal/Portal';
|
||||||
import Transition from 'react-transition-group/Transition';
|
import Transition from 'react-transition-group/Transition';
|
||||||
import { PopperContent } from './PopperController';
|
import { PopperContent } from './PopperController';
|
||||||
|
|
||||||
@@ -17,12 +17,7 @@ const transitionStyles: { [key: string]: object } = {
|
|||||||
exiting: { opacity: 0, transitionDelay: '500ms' },
|
exiting: { opacity: 0, transitionDelay: '500ms' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenderPopperArrowFn = (
|
export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element;
|
||||||
props: {
|
|
||||||
arrowProps: PopperArrowProps;
|
|
||||||
placement: string;
|
|
||||||
}
|
|
||||||
) => JSX.Element;
|
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -35,8 +30,16 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
|
|
||||||
class Popper extends PureComponent<Props> {
|
class Popper extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
|
const {
|
||||||
const { content } = this.props;
|
content,
|
||||||
|
show,
|
||||||
|
placement,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
className,
|
||||||
|
wrapperClassName,
|
||||||
|
renderArrow,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<Manager>
|
||||||
@@ -65,11 +68,12 @@ class Popper extends PureComponent<Props> {
|
|||||||
className={`${wrapperClassName}`}
|
className={`${wrapperClassName}`}
|
||||||
>
|
>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{typeof content === 'string'
|
{typeof content === 'string' && content}
|
||||||
? content
|
{React.isValidElement(content) && React.cloneElement(content)}
|
||||||
: React.cloneElement(content, {
|
{typeof content === 'function' &&
|
||||||
updatePopperPosition: scheduleUpdate,
|
content({
|
||||||
})}
|
updatePopperPosition: scheduleUpdate,
|
||||||
|
})}
|
||||||
{renderArrow &&
|
{renderArrow &&
|
||||||
renderArrow({
|
renderArrow({
|
||||||
arrowProps,
|
arrowProps,
|
||||||
@@ -89,4 +93,4 @@ class Popper extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Popper;
|
export { Popper };
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface PopperContentProps {
|
|||||||
updatePopperPosition?: () => void;
|
updatePopperPosition?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
|
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T> | ((props: T) => JSX.Element);
|
||||||
|
|
||||||
export interface UsingPopperProps {
|
export interface UsingPopperProps {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
@@ -101,4 +101,4 @@ class PopperController extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PopperController;
|
export { PopperController };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import * as PopperJS from 'popper.js';
|
import * as PopperJS from 'popper.js';
|
||||||
import Popper from './Popper';
|
import { Popper } from './Popper';
|
||||||
import PopperController, { UsingPopperProps } from './PopperController';
|
import { PopperController, UsingPopperProps } from './PopperController';
|
||||||
|
|
||||||
interface TooltipProps extends UsingPopperProps {
|
interface TooltipProps extends UsingPopperProps {
|
||||||
theme?: 'info' | 'error';
|
theme?: 'info' | 'error';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||||
export { Tooltip } from './Tooltip/Tooltip';
|
export { Tooltip } from './Tooltip/Tooltip';
|
||||||
|
export { PopperController } from './Tooltip/PopperController';
|
||||||
|
export { Popper } from './Tooltip/Popper';
|
||||||
export { Portal } from './Portal/Portal';
|
export { Portal } from './Portal/Portal';
|
||||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ let themeMock: ((name?: string) => GrafanaTheme) | null;
|
|||||||
|
|
||||||
export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
|
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;
|
themeMock = mock;
|
||||||
return () => {
|
return () => {
|
||||||
themeMock = null;
|
themeMock = null;
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ export interface DataQuery {
|
|||||||
datasource?: string | null;
|
datasource?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataQueryError {
|
||||||
|
data?: {
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
status?: string;
|
||||||
|
statusText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
|
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
|
||||||
timezone: string;
|
timezone: string;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ComponentClass } from 'react';
|
||||||
import { TimeSeries, LoadingState, TableData } from './data';
|
import { TimeSeries, LoadingState, TableData } from './data';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
|
|
||||||
@@ -19,11 +20,29 @@ export interface PanelData {
|
|||||||
tableData?: TableData;
|
tableData?: TableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelOptionsProps<T = any> {
|
export interface PanelEditorProps<T = any> {
|
||||||
options: T;
|
options: T;
|
||||||
onChange: (options: T) => void;
|
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 {
|
export interface PanelSize {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentClass } from 'react';
|
import { ComponentClass } from 'react';
|
||||||
import { PanelProps, PanelOptionsProps } from './panel';
|
import { ReactPanelPlugin } from './panel';
|
||||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
||||||
|
|
||||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||||
@@ -81,9 +81,7 @@ export interface PluginExports {
|
|||||||
|
|
||||||
// Panel plugin
|
// Panel plugin
|
||||||
PanelCtrl?: any;
|
PanelCtrl?: any;
|
||||||
Panel?: ComponentClass<PanelProps>;
|
reactPanel: ReactPanelPlugin;
|
||||||
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
|
||||||
PanelDefaults?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginMeta {
|
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) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return toFixed(size, decimals) + '%';
|
return toFixed(size, decimals) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPercentUnit(size: number, decimals: number) {
|
export function toPercentUnit(size: number, decimals: DecimalCount) {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return toFixed(100 * size, decimals) + '%';
|
return toFixed(100 * size, decimals) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toHex0x(value: number, decimals: number) {
|
export function toHex0x(value: number, decimals: DecimalCount) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export function toHex0x(value: number, decimals: number) {
|
|||||||
return '0x' + hexString;
|
return '0x' + hexString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toHex(value: number, decimals: number) {
|
export function toHex(value: number, decimals: DecimalCount) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -34,9 +34,9 @@ export function toHex(value: number, decimals: number) {
|
|||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sci(value: number, decimals: number) {
|
export function sci(value: number, decimals: DecimalCount) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return '';
|
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';
|
import moment from 'moment';
|
||||||
|
|
||||||
interface IntervalsInSeconds {
|
interface IntervalsInSeconds {
|
||||||
@@ -27,7 +27,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
|
|||||||
[Interval.Millisecond]: 0.001,
|
[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) {
|
if (size === null) {
|
||||||
return '';
|
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) {
|
if (size === null) {
|
||||||
return '';
|
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) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -83,22 +83,29 @@ export function toMilliSeconds(size: number, decimals: number, scaledDecimals: n
|
|||||||
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
|
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) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Less than 1 µs, divide in ns
|
// Less than 1 µs, divide in ns
|
||||||
if (Math.abs(size) < 0.000001) {
|
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
|
// Less than 1 ms, divide in µs
|
||||||
if (Math.abs(size) < 0.001) {
|
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
|
// Less than 1 second, divide in ms
|
||||||
if (Math.abs(size) < 1) {
|
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) {
|
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');
|
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) {
|
if (size === null) {
|
||||||
return '';
|
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) {
|
if (size === null) {
|
||||||
return '';
|
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) {
|
if (size === null) {
|
||||||
return '';
|
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) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size === 0) {
|
if (size === 0) {
|
||||||
return '0 ' + timeScale + 's';
|
return '0 ' + timeScale + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size < 0) {
|
if (size < 0) {
|
||||||
return toDuration(-size, decimals, timeScale) + ' ago';
|
return toDuration(-size, decimals, timeScale) + ' ago';
|
||||||
}
|
}
|
||||||
@@ -189,14 +198,22 @@ export function toDuration(size: number, decimals: number, timeScale: Interval):
|
|||||||
{ long: Interval.Second },
|
{ long: Interval.Second },
|
||||||
{ long: Interval.Millisecond },
|
{ long: Interval.Millisecond },
|
||||||
];
|
];
|
||||||
|
|
||||||
// convert $size to milliseconds
|
// convert $size to milliseconds
|
||||||
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
|
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
|
||||||
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
|
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
|
||||||
|
|
||||||
const strings = [];
|
const strings = [];
|
||||||
|
|
||||||
// after first value >= 1 print only $decimals more
|
// after first value >= 1 print only $decimals more
|
||||||
let decrementDecimals = false;
|
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 interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
|
||||||
const value = size / interval;
|
const value = size / interval;
|
||||||
if (value >= 1 || decrementDecimals) {
|
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' : '');
|
const unit = units[i].long + (floor !== 1 ? 's' : '');
|
||||||
strings.push(floor + ' ' + unit);
|
strings.push(floor + ' ' + unit);
|
||||||
size = size % interval;
|
size = size % interval;
|
||||||
decimals--;
|
decimalsCount--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.join(', ');
|
return strings.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toClock(size: number, decimals?: number) {
|
export function toClock(size: number, decimals?: DecimalCount) {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -257,11 +274,11 @@ export function toClock(size: number, decimals?: number) {
|
|||||||
return format ? `${hours}:${moment.utc(size).format(format)}` : hours;
|
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);
|
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);
|
return toDuration(size, decimals, Interval.Second);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,19 +293,19 @@ export function toDurationInHoursMinutesSeconds(size: number) {
|
|||||||
return strings.join(':');
|
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);
|
return toSeconds(size, decimals, scaledDecimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toClockMilliseconds(size: number, decimals: number) {
|
export function toClockMilliseconds(size: number, decimals: DecimalCount) {
|
||||||
return toClock(size, decimals);
|
return toClock(size, decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toClockSeconds(size: number, decimals: number) {
|
export function toClockSeconds(size: number, decimals: DecimalCount) {
|
||||||
return toClock(size * 1000, decimals);
|
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);
|
const time = isUtc ? moment.utc(value) : moment(value);
|
||||||
|
|
||||||
if (moment().isSame(value, 'day')) {
|
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');
|
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);
|
const time = isUtc ? moment.utc(value) : moment(value);
|
||||||
|
|
||||||
if (moment().isSame(value, 'day')) {
|
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');
|
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);
|
const time = isUtc ? moment.utc(value) : moment(value);
|
||||||
return time.fromNow();
|
return time.fromNow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { scaledUnits } from './valueFormats';
|
import { scaledUnits, DecimalCount } from './valueFormats';
|
||||||
|
|
||||||
export function currency(symbol: string) {
|
export function currency(symbol: string) {
|
||||||
const units = ['', 'K', 'M', 'B', 'T'];
|
const units = ['', 'K', 'M', 'B', 'T'];
|
||||||
const scaler = scaledUnits(1000, units);
|
const scaler = scaledUnits(1000, units);
|
||||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { getCategories } from './categories';
|
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;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
fn: ValueFormatter;
|
fn: ValueFormatter;
|
||||||
@@ -22,7 +29,7 @@ let categories: ValueFormatCategory[] = [];
|
|||||||
const index: ValueFormatterIndex = {};
|
const index: ValueFormatterIndex = {};
|
||||||
let hasBuiltIndex = false;
|
let hasBuiltIndex = false;
|
||||||
|
|
||||||
export function toFixed(value: number, decimals?: number): string {
|
export function toFixed(value: number, decimals?: DecimalCount): string {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -50,20 +57,24 @@ export function toFixed(value: number, decimals?: number): string {
|
|||||||
|
|
||||||
export function toFixedScaled(
|
export function toFixedScaled(
|
||||||
value: number,
|
value: number,
|
||||||
decimals: number,
|
decimals?: DecimalCount,
|
||||||
scaledDecimals: number,
|
scaledDecimals?: DecimalCount,
|
||||||
additionalDecimals: number,
|
additionalDecimals?: DecimalCount,
|
||||||
ext: string
|
ext?: string
|
||||||
) {
|
) {
|
||||||
if (scaledDecimals === null) {
|
if (scaledDecimals) {
|
||||||
return toFixed(value, decimals) + ext;
|
if (additionalDecimals) {
|
||||||
} else {
|
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
|
||||||
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
|
} else {
|
||||||
|
return toFixed(value, scaledDecimals) + ext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return toFixed(value, decimals) + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toFixedUnit(unit: string) {
|
export function toFixedUnit(unit: string): ValueFormatter {
|
||||||
return (size: number, decimals: number) => {
|
return (size: number, decimals?: DecimalCount) => {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -75,7 +86,7 @@ export function toFixedUnit(unit: string) {
|
|||||||
// numeric factor. Repeatedly scales the value down by the factor until it is
|
// 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.
|
// less than the factor in magnitude, or the end of the array is reached.
|
||||||
export function scaledUnits(factor: number, extArray: string[]) {
|
export function scaledUnits(factor: number, extArray: string[]) {
|
||||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
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;
|
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) {
|
if (value == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return value.toLocaleString(undefined, { maximumFractionDigits: decimals });
|
return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function simpleCountUnit(symbol: string) {
|
export function simpleCountUnit(symbol: string) {
|
||||||
const units = ['', 'K', 'M', 'B', 'T'];
|
const units = ['', 'K', 'M', 'B', 'T'];
|
||||||
const scaler = scaledUnits(1000, units);
|
const scaler = scaledUnits(1000, units);
|
||||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||||
if (size === null) {
|
if (size === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"include": [
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"src/**/*.ts",
|
"exclude": ["dist", "node_modules"],
|
||||||
"src/**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist",
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDirs": [".", "stories"],
|
"rootDirs": [".", "stories"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"typeRoots": ["./node_modules/@types", "types"],
|
"typeRoots": ["./node_modules/@types", "types"],
|
||||||
"skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
|
"skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
Text: "Explore",
|
Text: "Explore",
|
||||||
Id: "explore",
|
Id: "explore",
|
||||||
SubTitle: "Explore your data",
|
SubTitle: "Explore your data",
|
||||||
Icon: "fa fa-rocket",
|
Icon: "gicon gicon-explore",
|
||||||
Url: setting.AppSubUrl + "/explore",
|
Url: setting.AppSubUrl + "/explore",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ func installCommand(c CommandLine) error {
|
|||||||
return InstallPlugin(pluginToInstall, version, c)
|
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 {
|
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
||||||
pluginFolder := c.PluginDirectory()
|
pluginFolder := c.PluginDirectory()
|
||||||
downloadURL := c.PluginURL()
|
downloadURL := c.PluginURL()
|
||||||
@@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||||||
return err
|
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)))
|
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||||||
|
|
||||||
if zf.FileInfo().IsDir() {
|
if zf.FileInfo().IsDir() {
|
||||||
err := os.Mkdir(newFile, 0777)
|
err := os.Mkdir(newFile, 0777)
|
||||||
if PermissionsError(err) {
|
if permissionsError(err) {
|
||||||
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dst, err := os.Create(newFile)
|
fileMode := zf.Mode()
|
||||||
if PermissionsError(err) {
|
|
||||||
|
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)
|
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PermissionsError(err error) bool {
|
func permissionsError(err error) bool {
|
||||||
return err != nil && strings.Contains(err.Error(), "permission denied")
|
return err != nil && strings.Contains(err.Error(), "permission denied")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "diff":
|
case "diff":
|
||||||
var (
|
allNull, value = calculateDiff(series, allNull, value, diff)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "percent_diff":
|
case "percent_diff":
|
||||||
var (
|
allNull, value = calculateDiff(series, allNull, value, percentDiff)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "count_non_null":
|
case "count_non_null":
|
||||||
for _, v := range series.Points {
|
for _, v := range series.Points {
|
||||||
if v[0].Valid {
|
if v[0].Valid {
|
||||||
@@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
|||||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||||
return &SimpleReducer{Type: typ}
|
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))
|
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() {
|
Convey("percent_diff one point", func() {
|
||||||
result := testReducer("percent_diff", 40)
|
result := testReducer("percent_diff", 40)
|
||||||
So(result, ShouldEqual, float64(0))
|
So(result, ShouldEqual, float64(0))
|
||||||
@@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) {
|
|||||||
result := testReducer("percent_diff", 30, 40, 40)
|
result := testReducer("percent_diff", 30, 40, 40)
|
||||||
So(result, ShouldEqual, float64(33.33333333333333))
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
|||||||
return c.dashboardRef, nil
|
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) {
|
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||||
if c.IsTestRun {
|
if c.IsTestRun {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package alerting
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||||
@@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
|||||||
renderOpts := rendering.Opts{
|
renderOpts := rendering.Opts{
|
||||||
Width: 1000,
|
Width: 1000,
|
||||||
Height: 500,
|
Height: 500,
|
||||||
Timeout: alertTimeout / 2,
|
Timeout: time.Duration(float64(alertTimeout) * 0.9),
|
||||||
OrgId: context.Rule.OrgId,
|
OrgId: context.Rule.OrgId,
|
||||||
OrgRole: m.ROLE_ADMIN,
|
OrgRole: m.ROLE_ADMIN,
|
||||||
ConcurrentLimit: setting.AlertingRenderLimit,
|
ConcurrentLimit: setting.AlertingRenderLimit,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func init() {
|
|||||||
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
|
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
|
||||||
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
|
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
|
||||||
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
|
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
|
||||||
|
renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer}
|
||||||
|
|
||||||
renders["holt_winters"] = QueryDefinition{
|
renders["holt_winters"] = QueryDefinition{
|
||||||
Renderer: functionRenderer,
|
Renderer: functionRenderer,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
|
|||||||
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
|
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
|
||||||
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
|
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
|
||||||
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(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")}
|
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import { SideMenu } from './components/sidemenu/SideMenu';
|
|||||||
import { MetricSelect } from './components/Select/MetricSelect';
|
import { MetricSelect } from './components/Select/MetricSelect';
|
||||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||||
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
|
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
|
||||||
|
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
react2AngularDirective('sidemenu', SideMenu, []);
|
react2AngularDirective('sidemenu', SideMenu, []);
|
||||||
|
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
||||||
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||||
|
|||||||
@@ -61,15 +61,14 @@ export default class PermissionsListItem extends PureComponent<Props> {
|
|||||||
{item.name} <ItemDescription item={item} />
|
{item.name} <ItemDescription item={item} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{item.inherited &&
|
{item.inherited && folderInfo && (
|
||||||
folderInfo && (
|
<em className="muted no-wrap">
|
||||||
<em className="muted no-wrap">
|
Inherited from folder{' '}
|
||||||
Inherited from folder{' '}
|
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
||||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
{folderInfo.title}
|
||||||
{folderInfo.title}
|
</a>{' '}
|
||||||
</a>{' '}
|
</em>
|
||||||
</em>
|
)}
|
||||||
)}
|
|
||||||
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
||||||
</td>
|
</td>
|
||||||
<td className="query-keyword">Can</td>
|
<td className="query-keyword">Can</td>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Escapes `"` characters from string
|
* Escapes `"` characters from string
|
||||||
*/
|
*/
|
||||||
function escapeString(str: string): string {
|
function escapeString(str: string): string {
|
||||||
return str.replace('"', '"');
|
return str.replace('"', '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Determines if a value is an object
|
* Determines if a value is an object
|
||||||
*/
|
*/
|
||||||
export function isObject(value: any): boolean {
|
export function isObject(value: any): boolean {
|
||||||
const type = typeof value;
|
const type = typeof value;
|
||||||
return !!value && type === 'object';
|
return !!value && type === 'object';
|
||||||
@@ -20,7 +20,7 @@ export function isObject(value: any): boolean {
|
|||||||
* Gets constructor name of an object.
|
* Gets constructor name of an object.
|
||||||
* From http://stackoverflow.com/a/332429
|
* From http://stackoverflow.com/a/332429
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function getObjectName(object: object): string {
|
export function getObjectName(object: object): string {
|
||||||
if (object === undefined) {
|
if (object === undefined) {
|
||||||
return '';
|
return '';
|
||||||
@@ -43,7 +43,7 @@ export function getObjectName(object: object): string {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets type of an object. Returns "null" for null objects
|
* Gets type of an object. Returns "null" for null objects
|
||||||
*/
|
*/
|
||||||
export function getType(object: object): string {
|
export function getType(object: object): string {
|
||||||
if (object === null) {
|
if (object === null) {
|
||||||
return 'null';
|
return 'null';
|
||||||
@@ -53,7 +53,7 @@ export function getType(object: object): string {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates inline preview for a JavaScript object based on a value
|
* Generates inline preview for a JavaScript object based on a value
|
||||||
*/
|
*/
|
||||||
export function getValuePreview(object: object, value: string): string {
|
export function getValuePreview(object: object, value: string): string {
|
||||||
const type = getType(object);
|
const type = getType(object);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export function getValuePreview(object: object, value: string): string {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates inline preview for a JavaScript object
|
* Generates inline preview for a JavaScript object
|
||||||
*/
|
*/
|
||||||
let value = '';
|
let value = '';
|
||||||
export function getPreview(obj: object): string {
|
export function getPreview(obj: object): string {
|
||||||
if (isObject(obj)) {
|
if (isObject(obj)) {
|
||||||
@@ -94,15 +94,15 @@ export function getPreview(obj: object): string {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates a prefixed CSS class name
|
* Generates a prefixed CSS class name
|
||||||
*/
|
*/
|
||||||
export function cssClass(className: string): string {
|
export function cssClass(className: string): string {
|
||||||
return `json-formatter-${className}`;
|
return `json-formatter-${className}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Creates a new DOM element with given type and class
|
* Creates a new DOM element with given type and class
|
||||||
* TODO: move me to helpers
|
* TODO: move me to helpers
|
||||||
*/
|
*/
|
||||||
export function createElement(type: string, className?: string, content?: Element | string): Element {
|
export function createElement(type: string, className?: string, content?: Element | string): Element {
|
||||||
const el = document.createElement(type);
|
const el = document.createElement(type);
|
||||||
if (className) {
|
if (className) {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class JsonExplorer {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* is formatter open?
|
* is formatter open?
|
||||||
*/
|
*/
|
||||||
private get isOpen(): boolean {
|
private get isOpen(): boolean {
|
||||||
if (this._isOpen !== null) {
|
if (this._isOpen !== null) {
|
||||||
return this._isOpen;
|
return this._isOpen;
|
||||||
@@ -94,14 +94,14 @@ export class JsonExplorer {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* set open state (from toggler)
|
* set open state (from toggler)
|
||||||
*/
|
*/
|
||||||
private set isOpen(value: boolean) {
|
private set isOpen(value: boolean) {
|
||||||
this._isOpen = value;
|
this._isOpen = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* is this a date string?
|
* is this a date string?
|
||||||
*/
|
*/
|
||||||
private get isDate(): boolean {
|
private get isDate(): boolean {
|
||||||
return (
|
return (
|
||||||
this.type === 'string' &&
|
this.type === 'string' &&
|
||||||
@@ -111,14 +111,14 @@ export class JsonExplorer {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* is this a URL string?
|
* is this a URL string?
|
||||||
*/
|
*/
|
||||||
private get isUrl(): boolean {
|
private get isUrl(): boolean {
|
||||||
return this.type === 'string' && this.json.indexOf('http') === 0;
|
return this.type === 'string' && this.json.indexOf('http') === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* is this an array?
|
* is this an array?
|
||||||
*/
|
*/
|
||||||
private get isArray(): boolean {
|
private get isArray(): boolean {
|
||||||
return Array.isArray(this.json);
|
return Array.isArray(this.json);
|
||||||
}
|
}
|
||||||
@@ -126,21 +126,21 @@ export class JsonExplorer {
|
|||||||
/*
|
/*
|
||||||
* is this an object?
|
* is this an object?
|
||||||
* Note: In this context arrays are object as well
|
* Note: In this context arrays are object as well
|
||||||
*/
|
*/
|
||||||
private get isObject(): boolean {
|
private get isObject(): boolean {
|
||||||
return isObject(this.json);
|
return isObject(this.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* is this an empty object with no properties?
|
* is this an empty object with no properties?
|
||||||
*/
|
*/
|
||||||
private get isEmptyObject(): boolean {
|
private get isEmptyObject(): boolean {
|
||||||
return !this.keys.length && !this.isArray;
|
return !this.keys.length && !this.isArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* is this an empty object or array?
|
* is this an empty object or array?
|
||||||
*/
|
*/
|
||||||
private get isEmpty(): boolean {
|
private get isEmpty(): boolean {
|
||||||
return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
|
return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
|
||||||
}
|
}
|
||||||
@@ -148,14 +148,14 @@ export class JsonExplorer {
|
|||||||
/*
|
/*
|
||||||
* did we receive a key argument?
|
* did we receive a key argument?
|
||||||
* This means that the formatter was called as a sub formatter of a parent formatter
|
* This means that the formatter was called as a sub formatter of a parent formatter
|
||||||
*/
|
*/
|
||||||
private get hasKey(): boolean {
|
private get hasKey(): boolean {
|
||||||
return typeof this.key !== 'undefined';
|
return typeof this.key !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* if this is an object, get constructor function name
|
* if this is an object, get constructor function name
|
||||||
*/
|
*/
|
||||||
private get constructorName(): string {
|
private get constructorName(): string {
|
||||||
return getObjectName(this.json);
|
return getObjectName(this.json);
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ export class JsonExplorer {
|
|||||||
/*
|
/*
|
||||||
* get type of this value
|
* get type of this value
|
||||||
* Possible values: all JavaScript primitive types plus "array" and "null"
|
* Possible values: all JavaScript primitive types plus "array" and "null"
|
||||||
*/
|
*/
|
||||||
private get type(): string {
|
private get type(): string {
|
||||||
return getType(this.json);
|
return getType(this.json);
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ export class JsonExplorer {
|
|||||||
/*
|
/*
|
||||||
* get object keys
|
* get object keys
|
||||||
* If there is an empty key we pad it wit quotes to make it visible
|
* If there is an empty key we pad it wit quotes to make it visible
|
||||||
*/
|
*/
|
||||||
private get keys(): string[] {
|
private get keys(): string[] {
|
||||||
if (this.isObject) {
|
if (this.isObject) {
|
||||||
return Object.keys(this.json).map(key => (key ? key : '""'));
|
return Object.keys(this.json).map(key => (key ? key : '""'));
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class BottomNavLinks extends PureComponent<Props> {
|
|||||||
<div className="sidemenu-org-switcher__org-current">Current Org:</div>
|
<div className="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidemenu-org-switcher__switch">
|
<div className="sidemenu-org-switcher__switch">
|
||||||
<i className="fa fa-fw fa-random" />Switch
|
<i className="fa fa-fw fa-random" />
|
||||||
|
Switch
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export class SideMenu extends PureComponent {
|
|||||||
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||||
<i className="fa fa-bars" />
|
<i className="fa fa-bars" />
|
||||||
<span className="sidemenu__close">
|
<span className="sidemenu__close">
|
||||||
<i className="fa fa-times" /> Close
|
<i className="fa fa-times" />
|
||||||
|
Close
|
||||||
</span>
|
</span>
|
||||||
</div>,
|
</div>,
|
||||||
<TopSection key="topsection" />,
|
<TopSection key="topsection" />,
|
||||||
|
|||||||
@@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
|
|||||||
|
|
||||||
export const PANEL_HEADER_HEIGHT = 27;
|
export const PANEL_HEADER_HEIGHT = 27;
|
||||||
export const PANEL_BORDER = 2;
|
export const PANEL_BORDER = 2;
|
||||||
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function dropdownTypeahead2($compile) {
|
|||||||
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
||||||
|
|
||||||
const buttonTemplate =
|
const buttonTemplate =
|
||||||
'<a class="gf-form-input dropdown-toggle"' +
|
'<a class="{{buttonTemplateClass}} dropdown-toggle"' +
|
||||||
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
|
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
|
||||||
' ><i class="fa fa-plus"></i></a>';
|
' ><i class="fa fa-plus"></i></a>';
|
||||||
|
|
||||||
@@ -137,9 +137,15 @@ export function dropdownTypeahead2($compile) {
|
|||||||
menuItems: '=dropdownTypeahead2',
|
menuItems: '=dropdownTypeahead2',
|
||||||
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
|
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
|
||||||
model: '=ngModel',
|
model: '=ngModel',
|
||||||
|
buttonTemplateClass: '@',
|
||||||
},
|
},
|
||||||
link: ($scope, elem, attrs) => {
|
link: ($scope, elem, attrs) => {
|
||||||
const $input = $(inputTemplate);
|
const $input = $(inputTemplate);
|
||||||
|
|
||||||
|
if (!$scope.buttonTemplateClass) {
|
||||||
|
$scope.buttonTemplateClass = 'gf-form-input';
|
||||||
|
}
|
||||||
|
|
||||||
const $button = $(buttonTemplate);
|
const $button = $(buttonTemplate);
|
||||||
const timeoutId = {
|
const timeoutId = {
|
||||||
blur: null,
|
blur: null,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export class ValueSelectDropdownCtrl {
|
|||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
|
export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
|
||||||
return {
|
return {
|
||||||
scope: { variable: '=', onUpdated: '&' },
|
scope: { dashboard: '=', variable: '=', onUpdated: '&' },
|
||||||
templateUrl: 'public/app/partials/valueSelectDropdown.html',
|
templateUrl: 'public/app/partials/valueSelectDropdown.html',
|
||||||
controller: 'ValueSelectDropdownCtrl',
|
controller: 'ValueSelectDropdownCtrl',
|
||||||
controllerAs: 'vm',
|
controllerAs: 'vm',
|
||||||
@@ -288,13 +288,13 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanUp = $rootScope.$on('template-variable-value-updated', () => {
|
scope.vm.dashboard.on(
|
||||||
scope.vm.updateLinkText();
|
'template-variable-value-updated',
|
||||||
});
|
() => {
|
||||||
|
scope.vm.updateLinkText();
|
||||||
scope.$on('$destroy', () => {
|
},
|
||||||
cleanUp();
|
scope
|
||||||
});
|
);
|
||||||
|
|
||||||
scope.vm.init();
|
scope.vm.init();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export class AngularLoader {
|
|||||||
compiledElem.remove();
|
compiledElem.remove();
|
||||||
},
|
},
|
||||||
digest: () => {
|
digest: () => {
|
||||||
scope.$digest();
|
if (!scope.$$phase) {
|
||||||
|
scope.$digest();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getScope: () => {
|
getScope: () => {
|
||||||
return scope;
|
return scope;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function buildQueryTransaction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
|
||||||
|
|
||||||
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
||||||
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ kbn.secondsToHhmmss = seconds => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
kbn.to_percent = (nr, outof) => {
|
kbn.to_percent = (nr, outof) => {
|
||||||
return Math.floor(nr / outof * 10000) / 100 + '%';
|
return Math.floor((nr / outof) * 10000) / 100 + '%';
|
||||||
};
|
};
|
||||||
|
|
||||||
kbn.addslashes = str => {
|
kbn.addslashes = str => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent<Props> {
|
|||||||
'fa-pause': rule.state !== 'paused',
|
'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 (
|
return (
|
||||||
<li className="alert-rule-item">
|
<li className="alert-rule-item">
|
||||||
|
|||||||
@@ -149,4 +149,9 @@ const mapDispatchToProps = {
|
|||||||
togglePauseAlertRule,
|
togglePauseAlertRule,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AlertRuleList)
|
||||||
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`Render should render component 1`] = `
|
|||||||
className="alert-rule-item__name"
|
className="alert-rule-item__name"
|
||||||
>
|
>
|
||||||
<a
|
<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
|
<Highlighter
|
||||||
highlightClassName="highlight-search-match"
|
highlightClassName="highlight-search-match"
|
||||||
@@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
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"
|
title="Edit alert rule"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -263,4 +263,9 @@ const mapDispatchToProps = {
|
|||||||
addApiKey,
|
addApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ApiKeysPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
.add-panel-widget__title {
|
.add-panel-widget__title {
|
||||||
font-size: $font-size-md;
|
font-size: $font-size-md;
|
||||||
font-weight: $font-weight-semi-bold;
|
font-weight: $font-weight-semi-bold;
|
||||||
margin-right: $spacer*2;
|
margin-right: $spacer * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-panel-widget__link {
|
.add-panel-widget__link {
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
|
|
||||||
<div className="navbar-buttons navbar-buttons--tv">
|
<div className="navbar-buttons navbar-buttons--tv">
|
||||||
<DashNavButton
|
<DashNavButton
|
||||||
tooltip="Cycke view mode"
|
tooltip="Cycle view mode"
|
||||||
classSuffix="tv"
|
classSuffix="tv"
|
||||||
icon="fa fa-desktop"
|
icon="fa fa-desktop"
|
||||||
onClick={this.onToggleTVMode}
|
onClick={this.onToggleTVMode}
|
||||||
@@ -267,4 +267,7 @@ const mapDispatchToProps = {
|
|||||||
updateLocation,
|
updateLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DashNav);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DashNav);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
||||||
{{variable.label || variable.name}}
|
{{variable.label || variable.name}}
|
||||||
</label>
|
</label>
|
||||||
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" dashboard="ctrl.dashboard" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
||||||
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
|
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
|
||||||
</div>
|
</div>
|
||||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>
|
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { DashboardPage, Props, State } from './DashboardPage';
|
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
|
||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
import { cleanUpDashboard } from '../state/actions';
|
import { cleanUpDashboard } from '../state/actions';
|
||||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||||
@@ -250,4 +250,36 @@ describe('DashboardPage', () => {
|
|||||||
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
|
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,
|
urlUid: state.location.routeParams.uid,
|
||||||
urlSlug: state.location.routeParams.slug,
|
urlSlug: state.location.routeParams.slug,
|
||||||
urlType: state.location.routeParams.type,
|
urlType: state.location.routeParams.type,
|
||||||
editview: state.location.query.editview,
|
editview: state.location.query.editview,
|
||||||
urlPanelId: state.location.query.panelId,
|
urlPanelId: state.location.query.panelId,
|
||||||
urlFolderId: state.location.query.folderId,
|
urlFolderId: state.location.query.folderId,
|
||||||
urlFullscreen: state.location.query.fullscreen === true,
|
urlFullscreen: !!state.location.query.fullscreen,
|
||||||
urlEdit: state.location.query.edit === true,
|
urlEdit: !!state.location.query.edit,
|
||||||
initPhase: state.dashboard.initPhase,
|
initPhase: state.dashboard.initPhase,
|
||||||
isInitSlow: state.dashboard.isInitSlow,
|
isInitSlow: state.dashboard.isInitSlow,
|
||||||
initError: state.dashboard.initError,
|
initError: state.dashboard.initError,
|
||||||
@@ -306,4 +306,9 @@ const mapDispatchToProps = {
|
|||||||
updateLocation,
|
updateLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DashboardPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -107,4 +107,9 @@ const mapDispatchToProps = {
|
|||||||
initDashboard,
|
initDashboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SoloPanelPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 17,
|
"schemaVersion": 18,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 17,
|
"schemaVersion": 18,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 17,
|
"schemaVersion": 18,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 17,
|
"schemaVersion": 18,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
@@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
],
|
],
|
||||||
"refresh": undefined,
|
"refresh": undefined,
|
||||||
"revision": undefined,
|
"revision": undefined,
|
||||||
"schemaVersion": 17,
|
"schemaVersion": 18,
|
||||||
"snapshot": undefined,
|
"snapshot": undefined,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": Array [],
|
"tags": Array [],
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderReactPanel() {
|
renderReactPanel() {
|
||||||
const { dashboard, panel } = this.props;
|
const { dashboard, panel, isFullscreen } = this.props;
|
||||||
const { plugin } = this.state;
|
const { plugin } = this.state;
|
||||||
|
|
||||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
|
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAngularPanel() {
|
renderAngularPanel() {
|
||||||
@@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
style={styles}
|
style={styles}
|
||||||
>
|
>
|
||||||
{plugin.exports.Panel && this.renderReactPanel()}
|
{plugin.exports.reactPanel && this.renderReactPanel()}
|
||||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Library
|
// Library
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Tooltip } from '@grafana/ui';
|
|
||||||
|
|
||||||
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
|
|
||||||
// Services
|
// Services
|
||||||
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
// Utils
|
// Utils
|
||||||
@@ -11,6 +9,7 @@ import kbn from 'app/core/utils/kbn';
|
|||||||
import {
|
import {
|
||||||
DataQueryOptions,
|
DataQueryOptions,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
DataQueryError,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PanelData,
|
PanelData,
|
||||||
TableData,
|
TableData,
|
||||||
@@ -18,8 +17,6 @@ import {
|
|||||||
TimeSeries,
|
TimeSeries,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
loading: LoadingState;
|
loading: LoadingState;
|
||||||
panelData: PanelData;
|
panelData: PanelData;
|
||||||
@@ -38,12 +35,12 @@ export interface Props {
|
|||||||
maxDataPoints?: number;
|
maxDataPoints?: number;
|
||||||
children: (r: RenderProps) => JSX.Element;
|
children: (r: RenderProps) => JSX.Element;
|
||||||
onDataResponse?: (data: DataQueryResponse) => void;
|
onDataResponse?: (data: DataQueryResponse) => void;
|
||||||
|
onError: (message: string, error: DataQueryError) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
isFirstLoad: boolean;
|
isFirstLoad: boolean;
|
||||||
loading: LoadingState;
|
loading: LoadingState;
|
||||||
errorMessage: string;
|
|
||||||
response: DataQueryResponse;
|
response: DataQueryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +58,6 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: LoadingState.NotStarted,
|
loading: LoadingState.NotStarted,
|
||||||
errorMessage: '',
|
|
||||||
response: {
|
response: {
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
@@ -100,6 +96,7 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
widthPixels,
|
widthPixels,
|
||||||
maxDataPoints,
|
maxDataPoints,
|
||||||
onDataResponse,
|
onDataResponse,
|
||||||
|
onError,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
@@ -111,7 +108,7 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ loading: LoadingState.Loading, errorMessage: '' });
|
this.setState({ loading: LoadingState.Loading });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ds = await this.dataSourceSrv.get(datasource);
|
const ds = await this.dataSourceSrv.get(datasource);
|
||||||
@@ -150,18 +147,22 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
isFirstLoad: false,
|
isFirstLoad: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Loading error', err);
|
console.log('DataPanel error', err);
|
||||||
this.onError('Request Error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onError = (errorMessage: string) => {
|
let message = 'Query error';
|
||||||
if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
|
|
||||||
this.setState({
|
if (err.message) {
|
||||||
loading: LoadingState.Error,
|
message = err.message;
|
||||||
isFirstLoad: false,
|
} else if (err.data && err.data.message) {
|
||||||
errorMessage: errorMessage,
|
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() {
|
render() {
|
||||||
const { queries } = this.props;
|
const { queries } = this.props;
|
||||||
const { loading, isFirstLoad } = this.state;
|
const { loading, isFirstLoad } = this.state;
|
||||||
|
|
||||||
const panelData = this.getPanelData();
|
const panelData = this.getPanelData();
|
||||||
|
|
||||||
if (isFirstLoad && loading === LoadingState.Loading) {
|
// do not render component until we have first data
|
||||||
return this.renderLoadingStates();
|
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||||
|
return this.renderLoadingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queries.length) {
|
if (!queries.length) {
|
||||||
@@ -201,46 +202,17 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderLoadingStates()}
|
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||||
<ErrorBoundary>
|
{this.props.children({ loading, panelData })}
|
||||||
{({ error, errorInfo }) => {
|
|
||||||
if (errorInfo) {
|
|
||||||
this.onError(error.message || DEFAULT_PLUGIN_ERROR);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.props.children({
|
|
||||||
loading,
|
|
||||||
panelData,
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLoadingStates(): JSX.Element {
|
private renderLoadingState(): JSX.Element {
|
||||||
const { loading, errorMessage } = this.state;
|
return (
|
||||||
if (loading === LoadingState.Loading) {
|
<div className="panel-loading">
|
||||||
return (
|
<i className="fa fa-spinner fa-spin" />
|
||||||
<div className="panel-loading">
|
</div>
|
||||||
<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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
|||||||
// Components
|
// Components
|
||||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||||
import { DataPanel } from './DataPanel';
|
import { DataPanel } from './DataPanel';
|
||||||
|
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
|
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
|
||||||
@@ -17,16 +18,18 @@ import { profiler } from 'app/core/profiler';
|
|||||||
// Types
|
// Types
|
||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { PanelPlugin } from 'app/types';
|
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 variables from 'sass/_variables.scss';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import { DataQueryResponse } from '@grafana/ui/src';
|
|
||||||
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
|
isFullscreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@@ -34,6 +37,7 @@ export interface State {
|
|||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
timeInfo?: string;
|
timeInfo?: string;
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
|
errorMessage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelChrome extends PureComponent<Props, State> {
|
export class PanelChrome extends PureComponent<Props, State> {
|
||||||
@@ -45,6 +49,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
refreshCounter: 0,
|
refreshCounter: 0,
|
||||||
renderCounter: 0,
|
renderCounter: 0,
|
||||||
|
errorMessage: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +93,33 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
if (this.props.dashboard.isSnapshot()) {
|
if (this.props.dashboard.isSnapshot()) {
|
||||||
this.props.panel.snapshotData = dataQueryResponse.data;
|
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() {
|
get isVisible() {
|
||||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
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 {
|
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
|
||||||
const { panel, plugin } = this.props;
|
const { panel, plugin } = this.props;
|
||||||
const { timeRange, renderCounter } = this.state;
|
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
|
// This is only done to increase a counter that is used by backend
|
||||||
// image rendering (phantomjs/headless chrome) to know when to capture image
|
// image rendering (phantomjs/headless chrome) to know when to capture image
|
||||||
@@ -124,9 +154,9 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
panelData={panelData}
|
panelData={panelData}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
|
||||||
width={width - 2 * variables.panelHorizontalPadding}
|
width={width - 2 * variables.panelhorizontalpadding}
|
||||||
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
|
||||||
renderCounter={renderCounter}
|
renderCounter={renderCounter}
|
||||||
onInterpolate={this.onInterpolate}
|
onInterpolate={this.onInterpolate}
|
||||||
/>
|
/>
|
||||||
@@ -150,6 +180,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
widthPixels={width}
|
widthPixels={width}
|
||||||
refreshCounter={refreshCounter}
|
refreshCounter={refreshCounter}
|
||||||
onDataResponse={this.onDataResponse}
|
onDataResponse={this.onDataResponse}
|
||||||
|
onError={this.onDataError}
|
||||||
>
|
>
|
||||||
{({ loading, panelData }) => {
|
{({ loading, panelData }) => {
|
||||||
return this.renderPanelPlugin(loading, panelData, width, height);
|
return this.renderPanelPlugin(loading, panelData, width, height);
|
||||||
@@ -163,8 +194,8 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, panel } = this.props;
|
const { dashboard, panel, isFullscreen } = this.props;
|
||||||
const { timeInfo } = this.state;
|
const { errorMessage, timeInfo } = this.state;
|
||||||
const { transparent } = panel;
|
const { transparent } = panel;
|
||||||
|
|
||||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||||
@@ -185,8 +216,18 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
description={panel.description}
|
description={panel.description}
|
||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
links={panel.links}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
import { PanelHeaderCorner } from './PanelHeaderCorner';
|
||||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ export interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
scopedVars?: string;
|
scopedVars?: string;
|
||||||
links?: [];
|
links?: [];
|
||||||
|
error?: string;
|
||||||
|
isFullscreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClickCoordinates {
|
interface ClickCoordinates {
|
||||||
@@ -68,10 +70,9 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isFullscreen = false;
|
const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props;
|
||||||
const isLoading = false;
|
|
||||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||||
const { panel, dashboard, timeInfo, scopedVars } = this.props;
|
|
||||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,13 +83,9 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
description={panel.description}
|
description={panel.description}
|
||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
links={panel.links}
|
links={panel.links}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
<div className={panelHeaderClass}>
|
<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-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
|
||||||
<div className="panel-title">
|
<div className="panel-title">
|
||||||
<span className="icon-gf panel-alert-icon" />
|
<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 { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
|
||||||
enum InfoModes {
|
enum InfoMode {
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
Info = 'Info',
|
Info = 'Info',
|
||||||
Links = 'Links',
|
Links = 'Links',
|
||||||
@@ -18,18 +18,22 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
scopedVars?: string;
|
scopedVars?: string;
|
||||||
links?: [];
|
links?: [];
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelHeaderCorner extends Component<Props> {
|
export class PanelHeaderCorner extends Component<Props> {
|
||||||
timeSrv: TimeSrv = getTimeSrv();
|
timeSrv: TimeSrv = getTimeSrv();
|
||||||
|
|
||||||
getInfoMode = () => {
|
getInfoMode = () => {
|
||||||
const { panel } = this.props;
|
const { panel, error } = this.props;
|
||||||
|
if (error) {
|
||||||
|
return InfoMode.Error;
|
||||||
|
}
|
||||||
if (!!panel.description) {
|
if (!!panel.description) {
|
||||||
return InfoModes.Info;
|
return InfoMode.Info;
|
||||||
}
|
}
|
||||||
if (panel.links && panel.links.length) {
|
if (panel.links && panel.links.length) {
|
||||||
return InfoModes.Links;
|
return InfoMode.Links;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -42,50 +46,54 @@ export class PanelHeaderCorner extends Component<Props> {
|
|||||||
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
||||||
const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
|
const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
|
||||||
|
|
||||||
const html = (
|
return (
|
||||||
<div className="markdown-html">
|
<div className="markdown-html">
|
||||||
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
|
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
|
||||||
{panel.links &&
|
{panel.links && panel.links.length > 0 && (
|
||||||
panel.links.length > 0 && (
|
<ul className="text-left">
|
||||||
<ul className="text-left">
|
{panel.links.map((link, idx) => {
|
||||||
{panel.links.map((link, idx) => {
|
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
|
||||||
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
|
return (
|
||||||
return (
|
<li key={idx}>
|
||||||
<li key={idx}>
|
<a className="panel-menu-link" href={info.href} target={info.target}>
|
||||||
<a className="panel-menu-link" href={info.href} target={info.target}>
|
{info.title}
|
||||||
{info.title}
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</ul>
|
||||||
</ul>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</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() {
|
render() {
|
||||||
const infoMode: InfoModes | undefined = this.getInfoMode();
|
const infoMode: InfoMode | undefined = this.getInfoMode();
|
||||||
|
|
||||||
if (!infoMode) {
|
if (!infoMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (infoMode === InfoMode.Error) {
|
||||||
<>
|
return this.renderCornerType(infoMode, this.props.error);
|
||||||
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
|
}
|
||||||
<Tooltip content={this.getInfoContent()} placement="bottom-start">
|
|
||||||
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
|
if (infoMode === InfoMode.Info) {
|
||||||
<i className="fa" />
|
return this.renderCornerType(infoMode, this.getInfoContent());
|
||||||
<span className="panel-info-corner-inner" />
|
}
|
||||||
</div>
|
|
||||||
</Tooltip>
|
return null;
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PanelHeaderCorner;
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import _ from 'lodash';
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelProps } from '@grafana/ui';
|
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||||
import { PanelPlugin } from 'app/types';
|
import { PanelPlugin } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exports: {
|
exports: {
|
||||||
Panel: NotFound,
|
reactPanel: new ReactPanelPlugin(NotFound),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<EditorTabBody heading="Panel Options" toolbarItems={[]}>
|
<EditorTabBody heading="General" toolbarItems={[]}>
|
||||||
<div ref={element => (this.element = element)} />
|
<div ref={element => (this.element = element)} />
|
||||||
</EditorTabBody>
|
</EditorTabBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ interface PanelEditorTab {
|
|||||||
const panelEditorTabTexts = {
|
const panelEditorTabTexts = {
|
||||||
[PanelEditorTabIds.Queries]: 'Queries',
|
[PanelEditorTabIds.Queries]: 'Queries',
|
||||||
[PanelEditorTabIds.Visualization]: 'Visualization',
|
[PanelEditorTabIds.Visualization]: 'Visualization',
|
||||||
[PanelEditorTabIds.Advanced]: 'Panel Options',
|
[PanelEditorTabIds.Advanced]: 'General',
|
||||||
[PanelEditorTabIds.Alert]: 'Alert',
|
[PanelEditorTabIds.Alert]: 'Alert',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||||
<div className="flex-grow-1" />
|
<div className="flex-grow-1" />
|
||||||
{!isAddingMixed && (
|
{!isAddingMixed && (
|
||||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
|
||||||
Add Query
|
Add Query
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -176,7 +176,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { panel } = this.props;
|
const { panel, dashboard } = this.props;
|
||||||
const { currentDS, scrollTop } = this.state;
|
const { currentDS, scrollTop } = this.state;
|
||||||
|
|
||||||
const queryInspector: EditorToolbarView = {
|
const queryInspector: EditorToolbarView = {
|
||||||
@@ -205,6 +205,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
dataSourceValue={query.datasource || panel.datasource}
|
dataSourceValue={query.datasource || panel.datasource}
|
||||||
key={query.refId}
|
key={query.refId}
|
||||||
panel={panel}
|
panel={panel}
|
||||||
|
dashboard={dashboard}
|
||||||
query={query}
|
query={query}
|
||||||
onChange={query => this.onQueryChange(query, index)}
|
onChange={query => this.onQueryChange(query, index)}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../state/PanelModel';
|
import { PanelModel } from '../state/PanelModel';
|
||||||
import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui';
|
import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui';
|
||||||
|
import { DashboardModel } from '../state/DashboardModel';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
|
dashboard: DashboardModel;
|
||||||
onAddQuery: (query?: DataQuery) => void;
|
onAddQuery: (query?: DataQuery) => void;
|
||||||
onRemoveQuery: (query: DataQuery) => void;
|
onRemoveQuery: (query: DataQuery) => void;
|
||||||
onMoveQuery: (query: DataQuery, direction: number) => void;
|
onMoveQuery: (query: DataQuery, direction: number) => void;
|
||||||
@@ -28,39 +30,69 @@ interface State {
|
|||||||
loadedDataSourceValue: string | null | undefined;
|
loadedDataSourceValue: string | null | undefined;
|
||||||
datasource: DataSourceApi | null;
|
datasource: DataSourceApi | null;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
angularScope: AngularQueryComponentScope | null;
|
hasTextEditMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryEditorRow extends PureComponent<Props, State> {
|
export class QueryEditorRow extends PureComponent<Props, State> {
|
||||||
element: HTMLElement | null = null;
|
element: HTMLElement | null = null;
|
||||||
|
angularScope: AngularQueryComponentScope | null;
|
||||||
angularQueryEditor: AngularComponent | null = null;
|
angularQueryEditor: AngularComponent | null = null;
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
datasource: null,
|
datasource: null,
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
angularScope: null,
|
|
||||||
loadedDataSourceValue: undefined,
|
loadedDataSourceValue: undefined,
|
||||||
|
hasTextEditMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadDatasource();
|
this.loadDatasource();
|
||||||
this.props.panel.events.on('refresh', this.onPanelRefresh);
|
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 = () => {
|
onPanelRefresh = () => {
|
||||||
if (this.state.angularScope) {
|
if (this.angularScope) {
|
||||||
this.state.angularScope.range = getTimeSrv().timeRange();
|
this.angularScope.range = getTimeSrv().timeRange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getAngularQueryComponentScope(): AngularQueryComponentScope {
|
getAngularQueryComponentScope(): AngularQueryComponentScope {
|
||||||
const { panel, query } = this.props;
|
const { panel, query, dashboard } = this.props;
|
||||||
const { datasource } = this.state;
|
const { datasource } = this.state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasource: datasource,
|
datasource: datasource,
|
||||||
target: query,
|
target: query,
|
||||||
panel: panel,
|
panel: panel,
|
||||||
|
dashboard: dashboard,
|
||||||
refresh: () => panel.refresh(),
|
refresh: () => panel.refresh(),
|
||||||
render: () => panel.render(),
|
render: () => panel.render(),
|
||||||
events: panel.events,
|
events: panel.events,
|
||||||
@@ -73,7 +105,11 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
const dataSourceSrv = getDatasourceSrv();
|
const dataSourceSrv = getDatasourceSrv();
|
||||||
const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
|
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() {
|
componentDidUpdate() {
|
||||||
@@ -98,21 +134,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
|
const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
|
||||||
|
|
||||||
this.angularQueryEditor = loader.load(this.element, scopeProps, template);
|
this.angularQueryEditor = loader.load(this.element, scopeProps, template);
|
||||||
|
this.angularScope = scopeProps.ctrl;
|
||||||
|
|
||||||
// give angular time to compile
|
// give angular time to compile
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({ angularScope: scopeProps.ctrl });
|
this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode });
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.panel.events.off('refresh', this.onPanelRefresh);
|
|
||||||
|
|
||||||
if (this.angularQueryEditor) {
|
|
||||||
this.angularQueryEditor.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleCollapse = () => {
|
onToggleCollapse = () => {
|
||||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
this.setState({ isCollapsed: !this.state.isCollapsed });
|
||||||
};
|
};
|
||||||
@@ -138,10 +167,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onToggleEditMode = () => {
|
onToggleEditMode = () => {
|
||||||
const { angularScope } = this.state;
|
if (this.angularScope && this.angularScope.toggleEditorMode) {
|
||||||
|
this.angularScope.toggleEditorMode();
|
||||||
if (angularScope && angularScope.toggleEditorMode) {
|
|
||||||
angularScope.toggleEditorMode();
|
|
||||||
this.angularQueryEditor.digest();
|
this.angularQueryEditor.digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +177,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get hasTextEditMode() {
|
|
||||||
const { angularScope } = this.state;
|
|
||||||
return angularScope && angularScope.toggleEditorMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveQuery = () => {
|
onRemoveQuery = () => {
|
||||||
this.props.onRemoveQuery(this.props.query);
|
this.props.onRemoveQuery(this.props.query);
|
||||||
};
|
};
|
||||||
@@ -171,10 +193,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderCollapsedText(): string | null {
|
renderCollapsedText(): string | null {
|
||||||
const { angularScope } = this.state;
|
if (this.angularScope && this.angularScope.getCollapsedText) {
|
||||||
|
return this.angularScope.getCollapsedText();
|
||||||
if (angularScope && angularScope.getCollapsedText) {
|
|
||||||
return angularScope.getCollapsedText();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -182,7 +202,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query, inMixedMode } = this.props;
|
const { query, inMixedMode } = this.props;
|
||||||
const { datasource, isCollapsed } = this.state;
|
const { datasource, isCollapsed, hasTextEditMode } = this.state;
|
||||||
const isDisabled = query.hide;
|
const isDisabled = query.hide;
|
||||||
|
|
||||||
const bodyClasses = classNames('query-editor-row__body gf-form-query', {
|
const bodyClasses = classNames('query-editor-row__body gf-form-query', {
|
||||||
@@ -212,7 +232,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="query-editor-row__actions">
|
<div className="query-editor-row__actions">
|
||||||
{this.hasTextEditMode && (
|
{hasTextEditMode && (
|
||||||
<button
|
<button
|
||||||
className="query-editor-row__action"
|
className="query-editor-row__action"
|
||||||
onClick={this.onToggleEditMode}
|
onClick={this.onToggleEditMode}
|
||||||
@@ -248,6 +268,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
export interface AngularQueryComponentScope {
|
export interface AngularQueryComponentScope {
|
||||||
target: DataQuery;
|
target: DataQuery;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
|
dashboard: DashboardModel;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
render: () => void;
|
render: () => void;
|
||||||
|
|||||||
@@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelDefaultOptions = () => {
|
getReactPanelOptions = () => {
|
||||||
const { panel, plugin } = this.props;
|
const { panel, plugin } = this.props;
|
||||||
|
return panel.getOptions(plugin.exports.reactPanel.defaults);
|
||||||
if (plugin.exports.PanelDefaults) {
|
|
||||||
return panel.getOptions(plugin.exports.PanelDefaults.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return panel.getOptions(plugin.exports.PanelDefaults);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderPanelOptions() {
|
renderPanelOptions() {
|
||||||
const { plugin, angularPanel } = this.props;
|
const { plugin, angularPanel } = this.props;
|
||||||
const { PanelOptions } = plugin.exports;
|
|
||||||
|
|
||||||
if (angularPanel) {
|
if (angularPanel) {
|
||||||
return <div ref={element => (this.element = element)} />;
|
return <div ref={element => (this.element = element)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (plugin.exports.reactPanel) {
|
||||||
<>
|
const PanelEditor = plugin.exports.reactPanel.editor;
|
||||||
{PanelOptions ? (
|
|
||||||
<PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
|
if (PanelEditor) {
|
||||||
) : (
|
return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||||
<p>Visualization has no options</p>
|
}
|
||||||
)}
|
}
|
||||||
</>
|
|
||||||
);
|
return <p>Visualization has no options</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
@@ -227,8 +227,8 @@ export class TimeSrv {
|
|||||||
const timespan = range.to.valueOf() - range.from.valueOf();
|
const timespan = range.to.valueOf() - range.from.valueOf();
|
||||||
const center = range.to.valueOf() - timespan / 2;
|
const center = range.to.valueOf() - timespan / 2;
|
||||||
|
|
||||||
const to = center + timespan * factor / 2;
|
const to = center + (timespan * factor) / 2;
|
||||||
const from = center - timespan * factor / 2;
|
const from = center - (timespan * factor) / 2;
|
||||||
|
|
||||||
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('dashboard schema version should be set to latest', () => {
|
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', () => {
|
it('graph thresholds should be migrated', () => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class DashboardMigrator {
|
|||||||
let i, j, k, n;
|
let i, j, k, n;
|
||||||
const oldVersion = this.dashboard.schemaVersion;
|
const oldVersion = this.dashboard.schemaVersion;
|
||||||
const panelUpgrades = [];
|
const panelUpgrades = [];
|
||||||
this.dashboard.schemaVersion = 17;
|
this.dashboard.schemaVersion = 18;
|
||||||
|
|
||||||
if (oldVersion === this.dashboard.schemaVersion) {
|
if (oldVersion === this.dashboard.schemaVersion) {
|
||||||
return;
|
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) {
|
if (panelUpgrades.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -457,7 +487,7 @@ export class DashboardMigrator {
|
|||||||
for (const panel of row.panels) {
|
for (const panel of row.panels) {
|
||||||
panel.span = panel.span || DEFAULT_PANEL_SPAN;
|
panel.span = panel.span || DEFAULT_PANEL_SPAN;
|
||||||
if (panel.minSpan) {
|
if (panel.minSpan) {
|
||||||
panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan);
|
panel.minSpan = Math.min(GRID_COLUMN_COUNT, (GRID_COLUMN_COUNT / 12) * panel.minSpan);
|
||||||
}
|
}
|
||||||
const panelWidth = Math.floor(panel.span) * widthFactor;
|
const panelWidth = Math.floor(panel.span) * widthFactor;
|
||||||
const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight;
|
const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight;
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ describe('PanelModel', () => {
|
|||||||
type: 'table',
|
type: 'table',
|
||||||
showColumns: true,
|
showColumns: true,
|
||||||
targets: [{ refId: 'A' }, { noRefId: 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);
|
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', () => {
|
describe('when changing panel type', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
model.changeType('graph', true);
|
model.changeType('graph', true);
|
||||||
@@ -55,5 +84,19 @@ describe('PanelModel', () => {
|
|||||||
expect(model.alert).toBe(undefined);
|
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
|
// Types
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
import { DataQuery, TimeSeries, Threshold } from '@grafana/ui';
|
||||||
import { DataQuery, TimeSeries } from '@grafana/ui';
|
|
||||||
import { TableData } from '@grafana/ui/src';
|
import { TableData } from '@grafana/ui/src';
|
||||||
|
|
||||||
export interface GridPos {
|
export interface GridPos {
|
||||||
@@ -47,8 +46,6 @@ const mustKeepProps: { [str: string]: boolean } = {
|
|||||||
timeFrom: true,
|
timeFrom: true,
|
||||||
timeShift: true,
|
timeShift: true,
|
||||||
hideTimeOverride: true,
|
hideTimeOverride: true,
|
||||||
maxDataPoints: true,
|
|
||||||
interval: true,
|
|
||||||
description: true,
|
description: true,
|
||||||
links: true,
|
links: true,
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
@@ -92,6 +89,9 @@ export class PanelModel {
|
|||||||
timeFrom?: any;
|
timeFrom?: any;
|
||||||
timeShift?: any;
|
timeShift?: any;
|
||||||
hideTimeOverride?: any;
|
hideTimeOverride?: any;
|
||||||
|
options: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
maxDataPoints?: number;
|
maxDataPoints?: number;
|
||||||
interval?: string;
|
interval?: string;
|
||||||
@@ -105,8 +105,6 @@ export class PanelModel {
|
|||||||
hasRefreshed: boolean;
|
hasRefreshed: boolean;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
cacheTimeout?: any;
|
cacheTimeout?: any;
|
||||||
|
|
||||||
// cache props between plugins
|
|
||||||
cachedPluginOptions?: any;
|
cachedPluginOptions?: any;
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
@@ -121,6 +119,8 @@ export class PanelModel {
|
|||||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||||
// queries must have refId
|
// queries must have refId
|
||||||
this.ensureQueryIds();
|
this.ensureQueryIds();
|
||||||
|
|
||||||
|
this.restoreInfintyForThresholds();
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureQueryIds() {
|
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) {
|
getOptions(panelDefaults) {
|
||||||
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
|
return _.defaultsDeep(this.options || {}, panelDefaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOptions(options: object) {
|
updateOptions(options: object) {
|
||||||
const update: any = {};
|
this.options = options;
|
||||||
update[this.getOptionsKey()] = options;
|
|
||||||
Object.assign(this, update);
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOptionsKey() {
|
|
||||||
return PANEL_OPTIONS_KEY_PREFIX + this.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSaveModel() {
|
getSaveModel() {
|
||||||
const model: any = {};
|
const model: any = {};
|
||||||
for (const property in this) {
|
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
|
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||||
if (fromAngularPanel) {
|
if (fromAngularPanel) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of _.keys(this)) {
|
// remove panel type specific options
|
||||||
if (mustKeepProps[key]) {
|
for (const key of _.keys(this)) {
|
||||||
continue;
|
if (mustKeepProps[key]) {
|
||||||
}
|
continue;
|
||||||
|
|
||||||
delete this[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete this[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.restorePanelOptions(pluginId);
|
this.restorePanelOptions(pluginId);
|
||||||
|
|||||||
@@ -98,4 +98,9 @@ const mapDispatchToProps = {
|
|||||||
removeDashboard,
|
removeDashboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceDashboards));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DataSourceDashboards)
|
||||||
|
);
|
||||||
|
|||||||
@@ -115,4 +115,9 @@ const mapDispatchToProps = {
|
|||||||
setDataSourcesLayoutMode,
|
setDataSourcesLayoutMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DataSourcesListPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -80,4 +80,9 @@ const mapDispatchToProps = {
|
|||||||
setDataSourceTypeSearchQuery,
|
setDataSourceTypeSearchQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(NewDataSourcePage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(NewDataSourcePage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -259,4 +259,9 @@ const mapDispatchToProps = {
|
|||||||
setIsDefault,
|
setIsDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettingsPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DataSourceSettingsPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -200,43 +200,42 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{datasourceInstance &&
|
{datasourceInstance && !datasourceError && (
|
||||||
!datasourceError && (
|
<div className="explore-container">
|
||||||
<div className="explore-container">
|
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
||||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
<AutoSizer onResize={this.onResize} disableHeight>
|
||||||
<AutoSizer onResize={this.onResize} disableHeight>
|
{({ width }) => {
|
||||||
{({ width }) => {
|
if (width === 0) {
|
||||||
if (width === 0) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="m-t-2" style={{ width }}>
|
<main className="m-t-2" style={{ width }}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||||
{!showingStartPage && (
|
{!showingStartPage && (
|
||||||
<>
|
<>
|
||||||
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
|
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
|
||||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||||
{supportsLogs && (
|
{supportsLogs && (
|
||||||
<LogsContainer
|
<LogsContainer
|
||||||
width={width}
|
width={width}
|
||||||
exploreId={exploreId}
|
exploreId={exploreId}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
onClickLabel={this.onClickLabel}
|
onClickLabel={this.onClickLabel}
|
||||||
onStartScanning={this.onStartScanning}
|
onStartScanning={this.onStartScanning}
|
||||||
onStopScanning={this.onStopScanning}
|
onStopScanning={this.onStopScanning}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -287,4 +286,9 @@ const mapDispatchToProps = {
|
|||||||
setQueries,
|
setQueries,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Explore)
|
||||||
|
);
|
||||||
|
|||||||
@@ -103,18 +103,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
<div className="explore-toolbar-header-title">
|
<div className="explore-toolbar-header-title">
|
||||||
{exploreId === 'left' && (
|
{exploreId === 'left' && (
|
||||||
<span className="navbar-page-btn">
|
<span className="navbar-page-btn">
|
||||||
<i className="fa fa-rocket fa-fw" />
|
<i className="gicon gicon-explore" />
|
||||||
Explore
|
Explore
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="explore-toolbar-header-close">
|
{exploreId === 'right' && (
|
||||||
{exploreId === 'right' && (
|
<a className="explore-toolbar-header-close" onClick={this.props.closeSplit}>
|
||||||
<a onClick={this.props.closeSplit}>
|
<i className="fa fa-times fa-fw" />
|
||||||
<i className="fa fa-times fa-fw" />
|
</a>
|
||||||
</a>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="explore-toolbar-item">
|
<div className="explore-toolbar-item">
|
||||||
@@ -156,7 +154,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
splitted,
|
splitted,
|
||||||
title: 'Run Query',
|
title: 'Run Query',
|
||||||
onClick: this.onRunQuery,
|
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',
|
iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
|
||||||
iconSide: IconSide.right,
|
iconSide: IconSide.right,
|
||||||
})}
|
})}
|
||||||
@@ -195,4 +193,9 @@ const mapDispatchToProps: DispatchProps = {
|
|||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
|
export const ExploreToolbar = hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(UnConnectedExploreToolbar)
|
||||||
|
);
|
||||||
|
|||||||
@@ -217,11 +217,13 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
let series = [{ data: [[0, 0]] }];
|
let series = [{ data: [[0, 0]] }];
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
series = data.filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias)).map((ts: TimeSeries) => ({
|
series = data
|
||||||
color: ts.color,
|
.filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias))
|
||||||
label: ts.label,
|
.map((ts: TimeSeries) => ({
|
||||||
data: ts.getFlotPairs('null'),
|
color: ts.color,
|
||||||
}));
|
label: ts.label,
|
||||||
|
data: ts.getFlotPairs('null'),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dynamicOptions = this.getDynamicOptions();
|
this.dynamicOptions = this.getDynamicOptions();
|
||||||
@@ -242,17 +244,15 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.props.data &&
|
{this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && (
|
||||||
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
<div className="time-series-disclaimer">
|
||||||
!this.state.showAllTimeSeries && (
|
<i className="fa fa-fw fa-warning disclaimer-icon" />
|
||||||
<div className="time-series-disclaimer">
|
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
|
||||||
<i className="fa fa-fw fa-warning disclaimer-icon" />
|
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
|
||||||
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
|
this.props.data.length
|
||||||
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
|
}`}</span>
|
||||||
this.props.data.length
|
</div>
|
||||||
}`}</span>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div id={id} className="explore-graph" style={{ height }} />
|
<div id={id} className="explore-graph" style={{ height }} />
|
||||||
<Legend data={data} hiddenSeries={hiddenSeries} onToggleSeries={this.onToggleSeries} />
|
<Legend data={data} hiddenSeries={hiddenSeries} onToggleSeries={this.onToggleSeries} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -70,4 +70,9 @@ const mapDispatchToProps = {
|
|||||||
changeTime,
|
changeTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(GraphContainer)
|
||||||
|
);
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ export class LogLabelStats extends PureComponent<Props> {
|
|||||||
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
|
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
|
||||||
</div>
|
</div>
|
||||||
<div className="logs-stats__body">
|
<div className="logs-stats__body">
|
||||||
{topRows.map(stat => <LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />)}
|
{topRows.map(stat => (
|
||||||
|
<LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />
|
||||||
|
))}
|
||||||
{insertActiveRow && activeRow && <LogLabelStatsRow key={activeRow.value} {...activeRow} active />}
|
{insertActiveRow && activeRow && <LogLabelStatsRow key={activeRow.value} {...activeRow} active />}
|
||||||
{otherCount > 0 && (
|
{otherCount > 0 && (
|
||||||
<LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
|
<LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
|
||||||
|
|||||||
@@ -166,15 +166,14 @@ export class LogRow extends PureComponent<Props, State> {
|
|||||||
highlightClassName="logs-row__field-highlight"
|
highlightClassName="logs-row__field-highlight"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!parsed &&
|
{!parsed && needsHighlighter && (
|
||||||
needsHighlighter && (
|
<Highlighter
|
||||||
<Highlighter
|
textToHighlight={row.entry}
|
||||||
textToHighlight={row.entry}
|
searchWords={highlights}
|
||||||
searchWords={highlights}
|
findChunks={findHighlightChunksInText}
|
||||||
findChunks={findHighlightChunksInText}
|
highlightClassName={highlightClassName}
|
||||||
highlightClassName={highlightClassName}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
{!parsed && !needsHighlighter && row.entry}
|
{!parsed && !needsHighlighter && row.entry}
|
||||||
{showFieldStats && (
|
{showFieldStats && (
|
||||||
<div className="logs-row__stats">
|
<div className="logs-row__stats">
|
||||||
|
|||||||
@@ -237,17 +237,16 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasData &&
|
{hasData && meta && (
|
||||||
meta && (
|
<div className="logs-panel-meta">
|
||||||
<div className="logs-panel-meta">
|
{meta.map(item => (
|
||||||
{meta.map(item => (
|
<div className="logs-panel-meta__item" key={item.label}>
|
||||||
<div className="logs-panel-meta__item" key={item.label}>
|
<span className="logs-panel-meta__label">{item.label}:</span>
|
||||||
<span className="logs-panel-meta__label">{item.label}:</span>
|
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
|
||||||
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="logs-rows">
|
<div className="logs-rows">
|
||||||
{hasData &&
|
{hasData &&
|
||||||
@@ -282,16 +281,14 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
))}
|
))}
|
||||||
{hasData && deferLogs && <span>Rendering {dedupedData.rows.length} rows...</span>}
|
{hasData && deferLogs && <span>Rendering {dedupedData.rows.length} rows...</span>}
|
||||||
</div>
|
</div>
|
||||||
{!loading &&
|
{!loading && !hasData && !scanning && (
|
||||||
!hasData &&
|
<div className="logs-panel-nodata">
|
||||||
!scanning && (
|
No logs found.
|
||||||
<div className="logs-panel-nodata">
|
<a className="link" onClick={this.onClickScan}>
|
||||||
No logs found.
|
Scan for older logs
|
||||||
<a className="link" onClick={this.onClickScan}>
|
</a>
|
||||||
Scan for older logs
|
</div>
|
||||||
</a>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{scanning && (
|
{scanning && (
|
||||||
<div className="logs-panel-nodata">
|
<div className="logs-panel-nodata">
|
||||||
|
|||||||
@@ -127,4 +127,9 @@ const mapDispatchToProps = {
|
|||||||
toggleLogLevelAction,
|
toggleLogLevelAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(LogsContainer)
|
||||||
|
);
|
||||||
|
|||||||
@@ -169,4 +169,9 @@ const mapDispatchToProps = {
|
|||||||
runQueries,
|
runQueries,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(QueryRow)
|
||||||
|
);
|
||||||
|
|||||||
@@ -51,4 +51,9 @@ const mapDispatchToProps = {
|
|||||||
toggleTable,
|
toggleTable,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TableContainer)
|
||||||
|
);
|
||||||
|
|||||||
@@ -82,4 +82,9 @@ const mapDispatchToProps = {
|
|||||||
resetExploreAction,
|
resetExploreAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Wrapper)
|
||||||
|
);
|
||||||
|
|||||||
@@ -132,4 +132,9 @@ const mapDispatchToProps = {
|
|||||||
addFolderPermission,
|
addFolderPermission,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FolderPermissions)
|
||||||
|
);
|
||||||
|
|||||||
@@ -113,4 +113,9 @@ const mapDispatchToProps = {
|
|||||||
deleteFolder,
|
deleteFolder,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FolderSettingsPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -65,4 +65,9 @@ const mapDispatchToProps = {
|
|||||||
updateOrganization,
|
updateOrganization,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(OrgDetailsPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(OrgDetailsPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class QueryRowCtrl {
|
|||||||
this.target = this.queryCtrl.target;
|
this.target = this.queryCtrl.target;
|
||||||
this.panel = this.panelCtrl.panel;
|
this.panel = this.panelCtrl.panel;
|
||||||
|
|
||||||
if (this.hasTextEditMode) {
|
if (this.hasTextEditMode && this.queryCtrl.toggleEditorMode) {
|
||||||
// expose this function to react parent component
|
// expose this function to react parent component
|
||||||
this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl);
|
this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,4 +81,9 @@ const mapDispatchToProps = {
|
|||||||
setPluginsSearchQuery,
|
setPluginsSearchQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(PluginListPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -136,27 +136,29 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
// Datasource ConfigCtrl
|
// Datasource ConfigCtrl
|
||||||
case 'datasource-config-ctrl': {
|
case 'datasource-config-ctrl': {
|
||||||
const dsMeta = scope.ctrl.datasourceMeta;
|
const dsMeta = scope.ctrl.datasourceMeta;
|
||||||
return importPluginModule(dsMeta.module).then((dsModule): any => {
|
return importPluginModule(dsMeta.module).then(
|
||||||
if (!dsModule.ConfigCtrl) {
|
(dsModule): any => {
|
||||||
return { notFound: true };
|
if (!dsModule.ConfigCtrl) {
|
||||||
|
return { notFound: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$watch(
|
||||||
|
'ctrl.current',
|
||||||
|
() => {
|
||||||
|
scope.onModelChanged(scope.ctrl.current);
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: dsMeta.baseUrl,
|
||||||
|
name: 'ds-config-' + dsMeta.id,
|
||||||
|
bindings: { meta: '=', current: '=' },
|
||||||
|
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
|
||||||
|
Component: dsModule.ConfigCtrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
);
|
||||||
scope.$watch(
|
|
||||||
'ctrl.current',
|
|
||||||
() => {
|
|
||||||
scope.onModelChanged(scope.ctrl.current);
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: dsMeta.baseUrl,
|
|
||||||
name: 'ds-config-' + dsMeta.id,
|
|
||||||
bindings: { meta: '=', current: '=' },
|
|
||||||
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' },
|
|
||||||
Component: dsModule.ConfigCtrl,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// AppConfigCtrl
|
// AppConfigCtrl
|
||||||
case 'app-config-ctrl': {
|
case 'app-config-ctrl': {
|
||||||
|
|||||||
@@ -116,26 +116,25 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</SlideDown>
|
</SlideDown>
|
||||||
|
|
||||||
{groups.length === 0 &&
|
{groups.length === 0 && !isAdding && (
|
||||||
!isAdding && (
|
<div className="empty-list-cta">
|
||||||
<div className="empty-list-cta">
|
<div className="empty-list-cta__title">There are no external groups to sync with</div>
|
||||||
<div className="empty-list-cta__title">There are no external groups to sync with</div>
|
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-primary">
|
||||||
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-primary">
|
<i className="gicon gicon-add-team" />
|
||||||
<i className="gicon gicon-add-team" />
|
Add Group
|
||||||
Add Group
|
</button>
|
||||||
</button>
|
<div className="empty-list-cta__pro-tip">
|
||||||
<div className="empty-list-cta__pro-tip">
|
<i className="fa fa-rocket" /> {headerTooltip}
|
||||||
<i className="fa fa-rocket" /> {headerTooltip}
|
<a
|
||||||
<a
|
className="text-link empty-list-cta__pro-tip-link"
|
||||||
className="text-link empty-list-cta__pro-tip-link"
|
href="http://docs.grafana.org/auth/enhanced_ldap/"
|
||||||
href="http://docs.grafana.org/auth/enhanced_ldap/"
|
target="_blank"
|
||||||
target="_blank"
|
>
|
||||||
>
|
Learn more
|
||||||
Learn more
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
<div className="admin-list-table">
|
<div className="admin-list-table">
|
||||||
@@ -167,4 +166,7 @@ const mapDispatchToProps = {
|
|||||||
removeTeamGroup,
|
removeTeamGroup,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamGroupSync);
|
||||||
|
|||||||
@@ -161,4 +161,9 @@ const mapDispatchToProps = {
|
|||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamList)
|
||||||
|
);
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<td>
|
<td>
|
||||||
{labels.map(label => <TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />)}
|
{labels.map(label => (
|
||||||
|
<TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />
|
||||||
|
))}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -156,4 +158,7 @@ const mapDispatchToProps = {
|
|||||||
setSearchMemberQuery,
|
setSearchMemberQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamMembers);
|
||||||
|
|||||||
@@ -108,4 +108,9 @@ const mapDispatchToProps = {
|
|||||||
loadTeam,
|
loadTeam,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamPages)
|
||||||
|
);
|
||||||
|
|||||||
@@ -98,4 +98,7 @@ const mapDispatchToProps = {
|
|||||||
updateTeam,
|
updateTeam,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamSettings);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user