diff --git a/package.json b/package.json index 19939528425..afc51093823 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "6.0.0-beta2", + "version": "6.0.0-beta3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" @@ -121,7 +121,7 @@ "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "storybook": "cd packages/grafana-ui && yarn storybook", - "prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\"" + "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"" }, "husky": { "hooks": { @@ -129,14 +129,8 @@ } }, "lint-staged": { - "*.{ts,tsx,json,scss}": [ - "prettier --write", - "git add" - ], - "*pkg/**/*.go": [ - "gofmt -w -s", - "git add" - ] + "*.{ts,tsx,json,scss}": ["prettier --write", "git add"], + "*pkg/**/*.go": ["gofmt -w -s", "git add"] }, "prettier": { "trailingComma": "es5", @@ -201,12 +195,7 @@ "**/@types/react": "16.7.6" }, "workspaces": { - "packages": [ - "packages/*" - ], - "nohoist": [ - "**/@types/*", - "**/@types/*/**" - ] + "packages": ["packages/*"], + "nohoist": ["**/@types/*", "**/@types/*/**"] } } diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 5e5f477a733..70e29abc221 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -84,9 +84,9 @@ describe('Get thresholds formatted', () => { it('should get the correct formatted values when thresholds are added', () => { const { instance } = setup({ thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ], }); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index a7435a56b3c..2ec0517e96a 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -9,7 +9,7 @@ import { Themeable } from '../../index'; type TimeSeriesValue = string | number | null; export interface Props extends Themeable { - decimals: number; + decimals?: number | null; height: number; valueMappings: ValueMapping[]; maxValue: number; @@ -98,16 +98,15 @@ export class Gauge extends PureComponent { getFormattedThresholds() { const { maxValue, minValue, thresholds, theme } = this.props; - const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index); - const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1]; + const lastThreshold = thresholds[thresholds.length - 1]; return [ - ...thresholdsSortedByIndex.map(threshold => { + ...thresholds.map(threshold => { if (threshold.index === 0) { return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) }; } - const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; + const previousThreshold = thresholds[threshold.index - 1]; return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) }; }), { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) }, diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index ad038cebcda..d380ad26b68 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -22,7 +22,7 @@ export class Graph extends PureComponent { showBars: false, }; - element: HTMLElement | null; + element: HTMLElement | null = null; componentDidUpdate() { this.draw(); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 2b6af67df22..ea94537c429 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,9 +1,8 @@ import React, { ChangeEvent } from 'react'; -import { shallow } from 'enzyme'; - +import { mount } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; -const setup = (propOverrides?: object) => { +const setup = (propOverrides?: Partial) => { const props: Props = { onChange: jest.fn(), thresholds: [], @@ -11,12 +10,26 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - return shallow().instance() as ThresholdsEditor; + const wrapper = mount(); + const instance = wrapper.instance() as ThresholdsEditor; + + return { + instance, + wrapper, + }; }; +describe('Render', () => { + it('should render with base threshold', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + describe('Initialization', () => { it('should add a base threshold if missing', () => { - const instance = setup(); + const { instance } = setup(); expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); }); @@ -24,7 +37,7 @@ describe('Initialization', () => { describe('Add threshold', () => { it('should not add threshold at index 0', () => { - const instance = setup(); + const { instance } = setup(); instance.onAddThreshold(0); @@ -32,32 +45,32 @@ describe('Add threshold', () => { }); it('should add threshold', () => { - const instance = setup(); + const { instance } = setup(); instance.onAddThreshold(1); expect(instance.state.thresholds).toEqual([ - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, ]); }); it('should add another threshold above a first', () => { - const instance = setup({ + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], }); instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ]); }); it('should add another threshold between first and second index', () => { - const instance = setup({ + const { instance } = setup({ thresholds: [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, @@ -68,10 +81,10 @@ describe('Add threshold', () => { instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 3, value: 75, color: '#6ED0E0' }, - { index: 2, value: 62.5, color: '#EF843C' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 62.5, color: '#EF843C' }, + { index: 3, value: 75, color: '#6ED0E0' }, ]); }); }); @@ -83,7 +96,7 @@ describe('Remove threshold', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ thresholds }); + const { instance } = setup({ thresholds }); instance.onRemoveThreshold(thresholds[0]); @@ -96,9 +109,7 @@ describe('Remove threshold', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ - thresholds, - }); + const { instance } = setup({ thresholds }); instance.onRemoveThreshold(thresholds[1]); @@ -116,7 +127,7 @@ describe('change threshold value', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ thresholds }); + const { instance } = setup({ thresholds }); const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent; @@ -126,7 +137,7 @@ describe('change threshold value', () => { }); it('should update value', () => { - const instance = setup(); + const { instance } = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, @@ -150,24 +161,24 @@ describe('change threshold value', () => { }); describe('on blur threshold value', () => { - it('should resort rows and update indexes', () => { - const instance = setup(); + it.only('should resort rows and update indexes', () => { + const { instance } = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 78, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - instance.state = { + instance.setState({ thresholds, - }; + }); instance.onBlur(); expect(instance.state.thresholds).toEqual([ - { index: 2, value: 78, color: '#EAB839' }, - { index: 1, value: 75, color: '#6ED0E0' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 75, color: '#6ED0E0' }, + { index: 2, value: 78, color: '#EAB839' }, ]); }); }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index f4db23d6656..d613a911b03 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,7 +1,7 @@ import React, { PureComponent, ChangeEvent } from 'react'; import { Threshold } from '../../types'; -import { ColorPicker } from '../ColorPicker/ColorPicker'; -import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; +import { ColorPicker } from '..'; +import { PanelOptionsGroup } from '..'; import { colors } from '../../utils'; import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui'; @@ -54,16 +54,16 @@ export class ThresholdsEditor extends PureComponent { const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; // Set a color - const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0]; + const color = colors.filter(c => !newThresholds.some(t => t.color === c))[0]; this.setState( { thresholds: this.sortThresholds([ ...newThresholds, { + color, index, value: value as number, - color, }, ]), }, @@ -137,10 +137,11 @@ export class ThresholdsEditor extends PureComponent { onBlur = () => { this.setState(prevState => { const sortThresholds = this.sortThresholds([...prevState.thresholds]); - let index = sortThresholds.length - 1; + let index = 0; sortThresholds.forEach(t => { - t.index = index--; + t.index = index++; }); + return { thresholds: sortThresholds }; }); @@ -153,12 +154,11 @@ export class ThresholdsEditor extends PureComponent { sortThresholds = (thresholds: Threshold[]) => { return thresholds.sort((t1, t2) => { - return t2.value - t1.value; + return t1.value - t2.value; }); }; renderInput = (threshold: Threshold) => { - const value = threshold.index === 0 ? 'Base' : threshold.value; return (
@@ -169,51 +169,60 @@ export class ThresholdsEditor extends PureComponent {
)} -
- this.onChangeThresholdValue(event, threshold)} - value={value} - onBlur={this.onBlur} - readOnly={threshold.index === 0} - /> -
- {threshold.index > 0 && ( -
this.onRemoveThreshold(threshold)}> - + {threshold.index === 0 && ( +
+
)} + {threshold.index > 0 && ( + <> +
+ this.onChangeThresholdValue(event, threshold)} + value={threshold.value} + onBlur={this.onBlur} + readOnly={threshold.index === 0} + /> +
+
this.onRemoveThreshold(threshold)}> + +
+ + )}
); }; render() { const { thresholds } = this.state; - return ( {theme => { return (
- {thresholds.map((threshold, index) => { - return ( -
-
this.onAddThreshold(threshold.index + 1)} - > - + {thresholds + .slice(0) + .reverse() + .map((threshold, index) => { + return ( +
+
this.onAddThreshold(threshold.index + 1)} + > + +
+
+
{this.renderInput(threshold)}
-
-
{this.renderInput(threshold)}
-
- ); - })} + ); + })}
); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 8ef59bf08af..af70fd86f7a 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -43,7 +43,7 @@ } .thresholds-row-input { - margin-top: 49px; + margin-top: 44px; margin-left: 2px; } diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap new file mode 100644 index 00000000000..b0dc025090b --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render with base threshold 1`] = ` + + + +`; diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx index 93a896d97ed..cf4b9cdd653 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -35,8 +35,16 @@ interface Props extends React.HTMLAttributes { class Popper extends PureComponent { render() { - const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props; - const { content } = this.props; + const { + content, + show, + placement, + onMouseEnter, + onMouseLeave, + className, + wrapperClassName, + renderArrow, + } = this.props; return ( @@ -50,7 +58,7 @@ class Popper extends PureComponent { // TODO: move modifiers config to popper controller modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }} > - {({ ref, style, placement, arrowProps, scheduleUpdate }) => { + {({ ref, style, placement, arrowProps }) => { return (
{ className={`${wrapperClassName}`} >
- {typeof content === 'string' - ? content - : React.cloneElement(content, { - updatePopperPosition: scheduleUpdate, - })} + {typeof content === 'string' ? content : React.cloneElement(content)} {renderArrow && renderArrow({ arrowProps, diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index c0d9a4f2d32..1d8d2f62606 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -6,7 +6,7 @@ let themeMock: ((name?: string) => GrafanaTheme) | null; export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme); -export const mockTheme = (mock: (name: string) => GrafanaTheme) => { +export const mockTheme = (mock: (name?: string) => GrafanaTheme) => { themeMock = mock; return () => { themeMock = null; diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index e34cf25dc01..a34f39b59c6 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -29,6 +29,16 @@ export interface DataQuery { datasource?: string | null; } +export interface DataQueryError { + data?: { + message?: string; + error?: string; + }; + message?: string; + status?: string; + statusText?: string; +} + export interface DataQueryOptions { timezone: string; range: TimeRange; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 4eda85f9a28..2da48b0fec6 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,3 +1,4 @@ +import { ComponentClass } from 'react'; import { TimeSeries, LoadingState, TableData } from './data'; import { TimeRange } from './time'; @@ -19,11 +20,29 @@ export interface PanelData { tableData?: TableData; } -export interface PanelOptionsProps { +export interface PanelEditorProps { options: T; onChange: (options: T) => void; } +export class ReactPanelPlugin { + panel: ComponentClass>; + editor?: ComponentClass>; + defaults?: TOptions; + + constructor(panel: ComponentClass>) { + this.panel = panel; + } + + setEditor(editor: ComponentClass>) { + this.editor = editor; + } + + setDefaults(defaults: TOptions) { + this.defaults = defaults; + } +} + export interface PanelSize { width: number; height: number; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index c8f156c08dc..e2dda8ad407 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { PanelProps, PanelOptionsProps } from './panel'; +import { ReactPanelPlugin } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource'; export interface DataSourceApi { @@ -81,9 +81,7 @@ export interface PluginExports { // Panel plugin PanelCtrl?: any; - Panel?: ComponentClass; - PanelOptions?: ComponentClass; - PanelDefaults?: any; + reactPanel: ReactPanelPlugin; } export interface PluginMeta { diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts index fa9daf0fb97..8ff5a1ff5e7 100644 --- a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts @@ -1,20 +1,20 @@ -import { toFixed } from './valueFormats'; +import { toFixed, DecimalCount } from './valueFormats'; -export function toPercent(size: number, decimals: number) { +export function toPercent(size: number, decimals: DecimalCount) { if (size === null) { return ''; } return toFixed(size, decimals) + '%'; } -export function toPercentUnit(size: number, decimals: number) { +export function toPercentUnit(size: number, decimals: DecimalCount) { if (size === null) { return ''; } return toFixed(100 * size, decimals) + '%'; } -export function toHex0x(value: number, decimals: number) { +export function toHex0x(value: number, decimals: DecimalCount) { if (value == null) { return ''; } @@ -25,7 +25,7 @@ export function toHex0x(value: number, decimals: number) { return '0x' + hexString; } -export function toHex(value: number, decimals: number) { +export function toHex(value: number, decimals: DecimalCount) { if (value == null) { return ''; } @@ -34,9 +34,9 @@ export function toHex(value: number, decimals: number) { .toUpperCase(); } -export function sci(value: number, decimals: number) { +export function sci(value: number, decimals: DecimalCount) { if (value == null) { return ''; } - return value.toExponential(decimals); + return value.toExponential(decimals as number); } diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts index 1e07857eb66..819b73586d5 100644 --- a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts @@ -1,4 +1,4 @@ -import { toFixed, toFixedScaled } from './valueFormats'; +import { toFixed, toFixedScaled, DecimalCount } from './valueFormats'; import moment from 'moment'; interface IntervalsInSeconds { @@ -27,7 +27,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = { [Interval.Millisecond]: 0.001, }; -export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -45,7 +45,7 @@ export function toNanoSeconds(size: number, decimals: number, scaledDecimals: nu } } -export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -59,7 +59,7 @@ export function toMicroSeconds(size: number, decimals: number, scaledDecimals: n } } -export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -83,22 +83,29 @@ export function toMilliSeconds(size: number, decimals: number, scaledDecimals: n return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); } -export function toSeconds(size: number, decimals: number, scaledDecimals: number) { +export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount { + if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) { + return value1 - value2; + } + return undefined; +} + +export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } // Less than 1 µs, divide in ns if (Math.abs(size) < 0.000001) { - return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns'); } // Less than 1 ms, divide in µs if (Math.abs(size) < 0.001) { - return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs'); } // Less than 1 second, divide in ms if (Math.abs(size) < 1) { - return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms'); } if (Math.abs(size) < 60) { @@ -120,7 +127,7 @@ export function toSeconds(size: number, decimals: number, scaledDecimals: number return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); } -export function toMinutes(size: number, decimals: number, scaledDecimals: number) { +export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -138,7 +145,7 @@ export function toMinutes(size: number, decimals: number, scaledDecimals: number } } -export function toHours(size: number, decimals: number, scaledDecimals: number) { +export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -154,7 +161,7 @@ export function toHours(size: number, decimals: number, scaledDecimals: number) } } -export function toDays(size: number, decimals: number, scaledDecimals: number) { +export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -168,13 +175,15 @@ export function toDays(size: number, decimals: number, scaledDecimals: number) { } } -export function toDuration(size: number, decimals: number, timeScale: Interval): string { +export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string { if (size === null) { return ''; } + if (size === 0) { return '0 ' + timeScale + 's'; } + if (size < 0) { return toDuration(-size, decimals, timeScale) + ' ago'; } @@ -189,14 +198,22 @@ export function toDuration(size: number, decimals: number, timeScale: Interval): { long: Interval.Second }, { long: Interval.Millisecond }, ]; + // convert $size to milliseconds // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors size *= INTERVALS_IN_SECONDS[timeScale] * 1000; const strings = []; + // after first value >= 1 print only $decimals more let decrementDecimals = false; - for (let i = 0; i < units.length && decimals >= 0; i++) { + let decimalsCount = 0; + + if (decimals !== null || decimals !== undefined) { + decimalsCount = decimals as number; + } + + for (let i = 0; i < units.length && decimalsCount >= 0; i++) { const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000; const value = size / interval; if (value >= 1 || decrementDecimals) { @@ -205,14 +222,14 @@ export function toDuration(size: number, decimals: number, timeScale: Interval): const unit = units[i].long + (floor !== 1 ? 's' : ''); strings.push(floor + ' ' + unit); size = size % interval; - decimals--; + decimalsCount--; } } return strings.join(', '); } -export function toClock(size: number, decimals?: number) { +export function toClock(size: number, decimals?: DecimalCount) { if (size === null) { return ''; } @@ -257,11 +274,11 @@ export function toClock(size: number, decimals?: number) { return format ? `${hours}:${moment.utc(size).format(format)}` : hours; } -export function toDurationInMilliseconds(size: number, decimals: number) { +export function toDurationInMilliseconds(size: number, decimals: DecimalCount) { return toDuration(size, decimals, Interval.Millisecond); } -export function toDurationInSeconds(size: number, decimals: number) { +export function toDurationInSeconds(size: number, decimals: DecimalCount) { return toDuration(size, decimals, Interval.Second); } @@ -276,19 +293,19 @@ export function toDurationInHoursMinutesSeconds(size: number) { return strings.join(':'); } -export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) { +export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) { return toSeconds(size, decimals, scaledDecimals); } -export function toClockMilliseconds(size: number, decimals: number) { +export function toClockMilliseconds(size: number, decimals: DecimalCount) { return toClock(size, decimals); } -export function toClockSeconds(size: number, decimals: number) { +export function toClockSeconds(size: number, decimals: DecimalCount) { return toClock(size * 1000, decimals); } -export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); if (moment().isSame(value, 'day')) { @@ -297,7 +314,7 @@ export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: n return time.format('YYYY-MM-DD HH:mm:ss'); } -export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); if (moment().isSame(value, 'day')) { @@ -306,7 +323,7 @@ export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: nu return time.format('MM/DD/YYYY h:mm:ss a'); } -export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); return time.fromNow(); } diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts index 66808143daa..da8198daf5a 100644 --- a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts @@ -1,9 +1,9 @@ -import { scaledUnits } from './valueFormats'; +import { scaledUnits, DecimalCount } from './valueFormats'; export function currency(symbol: string) { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } diff --git a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts index 0a56ce58e5b..d626a241a79 100644 --- a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts +++ b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts @@ -1,8 +1,15 @@ import { getCategories } from './categories'; -type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string; +export type DecimalCount = number | null | undefined; -interface ValueFormat { +export type ValueFormatter = ( + value: number, + decimals?: DecimalCount, + scaledDecimals?: DecimalCount, + isUtc?: boolean +) => string; + +export interface ValueFormat { name: string; id: string; fn: ValueFormatter; @@ -22,7 +29,7 @@ let categories: ValueFormatCategory[] = []; const index: ValueFormatterIndex = {}; let hasBuiltIndex = false; -export function toFixed(value: number, decimals?: number): string { +export function toFixed(value: number, decimals?: DecimalCount): string { if (value === null) { return ''; } @@ -50,20 +57,24 @@ export function toFixed(value: number, decimals?: number): string { export function toFixedScaled( value: number, - decimals: number, - scaledDecimals: number, - additionalDecimals: number, - ext: string + decimals?: DecimalCount, + scaledDecimals?: DecimalCount, + additionalDecimals?: DecimalCount, + ext?: string ) { - if (scaledDecimals === null) { - return toFixed(value, decimals) + ext; - } else { - return toFixed(value, scaledDecimals + additionalDecimals) + ext; + if (scaledDecimals) { + if (additionalDecimals) { + return toFixed(value, scaledDecimals + additionalDecimals) + ext; + } else { + return toFixed(value, scaledDecimals) + ext; + } } + + return toFixed(value, decimals) + ext; } -export function toFixedUnit(unit: string) { - return (size: number, decimals: number) => { +export function toFixedUnit(unit: string): ValueFormatter { + return (size: number, decimals?: DecimalCount) => { if (size === null) { return ''; } @@ -75,7 +86,7 @@ export function toFixedUnit(unit: string) { // numeric factor. Repeatedly scales the value down by the factor until it is // less than the factor in magnitude, or the end of the array is reached. export function scaledUnits(factor: number, extArray: string[]) { - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } @@ -92,7 +103,7 @@ export function scaledUnits(factor: number, extArray: string[]) { } } - if (steps > 0 && scaledDecimals !== null) { + if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) { decimals = scaledDecimals + 3 * steps; } @@ -100,17 +111,17 @@ export function scaledUnits(factor: number, extArray: string[]) { }; } -export function locale(value: number, decimals: number) { +export function locale(value: number, decimals: DecimalCount) { if (value == null) { return ''; } - return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); + return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }); } export function simpleCountUnit(symbol: string) { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } diff --git a/packages/grafana-ui/tsconfig.json b/packages/grafana-ui/tsconfig.json index 22d336b85f5..1dfe6e0b44c 100644 --- a/packages/grafana-ui/tsconfig.json +++ b/packages/grafana-ui/tsconfig.json @@ -1,21 +1,17 @@ { "extends": "../../tsconfig.json", - "include": [ - "src/**/*.ts", - "src/**/*.tsx" - ], - "exclude": [ - "dist", - "node_modules" - ], + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules"], "compilerOptions": { "rootDirs": [".", "stories"], "module": "esnext", "outDir": "dist", "declaration": true, + "strict": true, + "alwaysStrict": true, "noImplicitAny": true, "strictNullChecks": true, "typeRoots": ["./node_modules/@types", "types"], "skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors - }, + } } diff --git a/pkg/api/index.go b/pkg/api/index.go index e90db84016d..248ebf63f0f 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -145,7 +145,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er Text: "Explore", Id: "explore", SubTitle: "Explore your data", - Icon: "fa fa-rocket", + Icon: "gicon gicon-explore", Url: setting.AppSubUrl + "/explore", }) } diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index f88bb9bbfff..d758633fea5 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -57,6 +57,8 @@ func installCommand(c CommandLine) error { return InstallPlugin(pluginToInstall, version, c) } +// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API +// and then extracts the zip into the plugins directory. func InstallPlugin(pluginName, version string, c CommandLine) error { pluginFolder := c.PluginDirectory() downloadURL := c.PluginURL() @@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) { return err } + return extractFiles(body, pluginName, filePath) +} + +func extractFiles(body []byte, pluginName string, filePath string) error { r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) if err != nil { return err @@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) { if zf.FileInfo().IsDir() { err := os.Mkdir(newFile, 0777) - if PermissionsError(err) { + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } } else { - dst, err := os.Create(newFile) - if PermissionsError(err) { + fileMode := zf.Mode() + + if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") { + fileMode = os.FileMode(0755) + } + + dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } @@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) { return nil } -func PermissionsError(err error) bool { +func permissionsError(err error) bool { return err != nil && strings.Contains(err.Error(), "permission denied") } diff --git a/pkg/cmd/grafana-cli/commands/install_command_test.go b/pkg/cmd/grafana-cli/commands/install_command_test.go index 52b329adf7f..3554dda82a9 100644 --- a/pkg/cmd/grafana-cli/commands/install_command_test.go +++ b/pkg/cmd/grafana-cli/commands/install_command_test.go @@ -1,6 +1,8 @@ package commands import ( + "io/ioutil" + "os" "testing" . "github.com/smartystreets/goconvey/convey" @@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) { }) }) } + +func TestExtractFiles(t *testing.T) { + Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() { + err := os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + err = os.MkdirAll("testdata/fake-plugins-dir", 0774) + So(err, ShouldBeNil) + + body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip") + So(err, ShouldBeNil) + + err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + //File in zip has permissions 777 + fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 664 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 644 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--") + + //File in zip has permission 755 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + err = os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + }) +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip b/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip new file mode 100644 index 00000000000..f9263ab3e78 Binary files /dev/null and b/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip differ diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 1e8ae792746..f55545be311 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { } } case "diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - value = first - points[i][0].Float64 - break - } - } + allNull, value = calculateDiff(series, allNull, value, diff) case "percent_diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 - value = math.Abs(val) - break - } - } + allNull, value = calculateDiff(series, allNull, value, percentDiff) case "count_non_null": for _, v := range series.Points { if v[0].Valid { @@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { func NewSimpleReducer(typ string) *SimpleReducer { return &SimpleReducer{Type: typ} } + +func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) { + var ( + points = series.Points + first float64 + i int + ) + // get the newest point + for i = len(points) - 1; i >= 0; i-- { + if points[i][0].Valid { + allNull = false + first = points[i][0].Float64 + break + } + } + if i >= 1 { + // get the oldest point + points = points[0:i] + for i := 0; i < len(points); i++ { + if points[i][0].Valid { + allNull = false + val := fn(first, points[i][0].Float64) + value = math.Abs(val) + break + } + } + } + return allNull, value +} + +var diff = func(newest, oldest float64) float64 { + return newest - oldest +} + +var percentDiff = func(newest, oldest float64) float64 { + return (newest - oldest) / oldest * 100 +} diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 7f11fc498bd..d2c21771d0b 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(10)) }) + Convey("diff with only nulls", func() { + reducer := NewSimpleReducer("diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) + Convey("percent_diff one point", func() { result := testReducer("percent_diff", 40) So(result, ShouldEqual, float64(0)) @@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) { result := testReducer("percent_diff", 30, 40, 40) So(result, ShouldEqual, float64(33.33333333333333)) }) + + Convey("percent_diff with only nulls", func() { + reducer := NewSimpleReducer("percent_diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) }) } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 4db942e0a55..02b9955662f 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { return c.dashboardRef, nil } -const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d" +const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d" func (c *EvalContext) GetRuleUrl() (string, error) { if c.IsTestRun { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index e1a550d48f4..59d459f122e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,6 +3,7 @@ package alerting import ( "errors" "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { renderOpts := rendering.Opts{ Width: 1000, Height: 500, - Timeout: alertTimeout / 2, + Timeout: time.Duration(float64(alertTimeout) * 0.9), OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, ConcurrentLimit: setting.AlertingRenderLimit, diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 29a77f15617..e81da28fd4d 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -33,6 +33,7 @@ func init() { renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["mode"] = QueryDefinition{Renderer: functionRenderer} renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer} + renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer} renders["holt_winters"] = QueryDefinition{ Renderer: functionRenderer, diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index 76daf6446d8..915c066b460 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -24,6 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) { {mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`}, {mode: "mode", params: []string{}, input: "value", expected: `mode(value)`}, {mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`}, + {mode: "non_negative_difference", params: []string{}, input: "max(value)", expected: `non_negative_difference(max(value))`}, } queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 7d295b27726..d51c4cf83d6 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20; export const PANEL_HEADER_HEIGHT = 27; export const PANEL_BORDER = 2; -export const PANEL_OPTIONS_KEY_PREFIX = 'options-'; diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts index 54dd9a35767..d9b78e66cba 100644 --- a/public/app/core/services/AngularLoader.ts +++ b/public/app/core/services/AngularLoader.ts @@ -27,7 +27,9 @@ export class AngularLoader { compiledElem.remove(); }, digest: () => { - scope.$digest(); + if (!scope.$$phase) { + scope.$digest(); + } }, getScope: () => { return scope; diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 86bb0207460..3fec37d19b8 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent { '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 (
  • diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap index f686127ebf3..8e076ffd22e 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Render should render component 1`] = ` className="alert-rule-item__name" > {
    { 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); + }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 27118e297b5..bdb601a692f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ +export const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, urlFolderId: state.location.query.folderId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, + urlFullscreen: !!state.location.query.fullscreen, + urlEdit: !!state.location.query.edit, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index 002cac2306e..f60e60c43a8 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b9c56e36382..9aeddd5a0d9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent { }; renderReactPanel() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { plugin } = this.state; - return ; + return ; } renderAngularPanel() { @@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent { onMouseLeave={this.onMouseLeave} style={styles} > - {plugin.exports.Panel && this.renderReactPanel()} + {plugin.exports.reactPanel && this.renderReactPanel()} {plugin.exports.PanelCtrl && this.renderAngularPanel()}
    )} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index b81d66fa7f5..9718e150e2a 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,8 +1,6 @@ // Library import React, { Component } from 'react'; -import { Tooltip } from '@grafana/ui'; -import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Utils @@ -11,6 +9,7 @@ import kbn from 'app/core/utils/kbn'; import { DataQueryOptions, DataQueryResponse, + DataQueryError, LoadingState, PanelData, TableData, @@ -18,8 +17,6 @@ import { TimeSeries, } from '@grafana/ui'; -const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; - interface RenderProps { loading: LoadingState; panelData: PanelData; @@ -38,12 +35,12 @@ export interface Props { maxDataPoints?: number; children: (r: RenderProps) => JSX.Element; onDataResponse?: (data: DataQueryResponse) => void; + onError: (message: string, error: DataQueryError) => void; } export interface State { isFirstLoad: boolean; loading: LoadingState; - errorMessage: string; response: DataQueryResponse; } @@ -61,7 +58,6 @@ export class DataPanel extends Component { this.state = { loading: LoadingState.NotStarted, - errorMessage: '', response: { data: [], }, @@ -100,6 +96,7 @@ export class DataPanel extends Component { widthPixels, maxDataPoints, onDataResponse, + onError, } = this.props; if (!isVisible) { @@ -111,7 +108,7 @@ export class DataPanel extends Component { return; } - this.setState({ loading: LoadingState.Loading, errorMessage: '' }); + this.setState({ loading: LoadingState.Loading }); try { const ds = await this.dataSourceSrv.get(datasource); @@ -150,18 +147,22 @@ export class DataPanel extends Component { isFirstLoad: false, }); } catch (err) { - console.log('Loading error', err); - this.onError('Request Error'); - } - }; + console.log('DataPanel error', err); - onError = (errorMessage: string) => { - if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { - this.setState({ - loading: LoadingState.Error, - isFirstLoad: false, - errorMessage: errorMessage, - }); + let message = 'Query error'; + + if (err.message) { + message = err.message; + } else if (err.data && err.data.message) { + message = err.data.message; + } else if (err.data && err.data.error) { + message = err.data.error; + } else if (err.status) { + message = `Query error: ${err.status} ${err.statusText}`; + } + + onError(message, err); + this.setState({ isFirstLoad: false, loading: LoadingState.Error }); } }; @@ -184,11 +185,11 @@ export class DataPanel extends Component { render() { const { queries } = this.props; const { loading, isFirstLoad } = this.state; - const panelData = this.getPanelData(); - if (isFirstLoad && loading === LoadingState.Loading) { - return this.renderLoadingStates(); + // do not render component until we have first data + if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { + return this.renderLoadingState(); } if (!queries.length) { @@ -201,46 +202,17 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingStates()} - - {({ error, errorInfo }) => { - if (errorInfo) { - this.onError(error.message || DEFAULT_PLUGIN_ERROR); - return null; - } - return ( - <> - {this.props.children({ - loading, - panelData, - })} - - ); - }} - + {loading === LoadingState.Loading && this.renderLoadingState()} + {this.props.children({ loading, panelData })} ); } - private renderLoadingStates(): JSX.Element { - const { loading, errorMessage } = this.state; - if (loading === LoadingState.Loading) { - return ( -
    - -
    - ); - } else if (loading === LoadingState.Error) { - return ( - -
    - - -
    -
    - ); - } - - return null; + private renderLoadingState(): JSX.Element { + return ( +
    + +
    + ); } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b050b1ff5e1..23c92b23837 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; +import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; // Utils import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; @@ -17,16 +18,18 @@ import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { TimeRange, LoadingState, PanelData } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui'; import variables from 'sass/_variables.scss'; import templateSrv from 'app/features/templating/template_srv'; -import { DataQueryResponse } from '@grafana/ui/src'; + +const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; export interface Props { panel: PanelModel; dashboard: DashboardModel; plugin: PanelPlugin; + isFullscreen: boolean; } export interface State { @@ -34,6 +37,7 @@ export interface State { renderCounter: number; timeInfo?: string; timeRange?: TimeRange; + errorMessage: string | null; } export class PanelChrome extends PureComponent { @@ -45,6 +49,7 @@ export class PanelChrome extends PureComponent { this.state = { refreshCounter: 0, renderCounter: 0, + errorMessage: null, }; } @@ -88,8 +93,33 @@ export class PanelChrome extends PureComponent { if (this.props.dashboard.isSnapshot()) { this.props.panel.snapshotData = dataQueryResponse.data; } + // clear error state (if any) + this.clearErrorState(); + + // This event is used by old query editors and panel editor options + this.props.panel.events.emit('data-received', dataQueryResponse.data); }; + onDataError = (message: string, error: DataQueryError) => { + if (this.state.errorMessage !== message) { + this.setState({ errorMessage: message }); + } + // this event is used by old query editors + this.props.panel.events.emit('data-error', error); + }; + + onPanelError = (message: string) => { + if (this.state.errorMessage !== message) { + this.setState({ errorMessage: message }); + } + }; + + clearErrorState() { + if (this.state.errorMessage) { + this.setState({ errorMessage: null }); + } + } + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } @@ -110,7 +140,7 @@ export class PanelChrome extends PureComponent { renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; - const PanelComponent = plugin.exports.Panel; + const PanelComponent = plugin.exports.reactPanel.panel; // This is only done to increase a counter that is used by backend // image rendering (phantomjs/headless chrome) to know when to capture image @@ -124,9 +154,9 @@ export class PanelChrome extends PureComponent { loading={loading} panelData={panelData} timeRange={timeRange} - options={panel.getOptions(plugin.exports.PanelDefaults)} - width={width - 2 * variables.panelHorizontalPadding} - height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding} + options={panel.getOptions(plugin.exports.reactPanel.defaults)} + width={width - 2 * variables.panelhorizontalpadding} + height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding} renderCounter={renderCounter} onInterpolate={this.onInterpolate} /> @@ -150,6 +180,7 @@ export class PanelChrome extends PureComponent { widthPixels={width} refreshCounter={refreshCounter} onDataResponse={this.onDataResponse} + onError={this.onDataError} > {({ loading, panelData }) => { return this.renderPanelPlugin(loading, panelData, width, height); @@ -163,8 +194,8 @@ export class PanelChrome extends PureComponent { }; render() { - const { dashboard, panel } = this.props; - const { timeInfo } = this.state; + const { dashboard, panel, isFullscreen } = this.props; + const { errorMessage, timeInfo } = this.state; const { transparent } = panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; @@ -185,8 +216,18 @@ export class PanelChrome extends PureComponent { description={panel.description} scopedVars={panel.scopedVars} links={panel.links} + error={errorMessage} + isFullscreen={isFullscreen} /> - {this.renderPanelBody(width, height)} + + {({ error, errorInfo }) => { + if (errorInfo) { + this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + return this.renderPanelBody(width, height); + }} +
  • ); }} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 22766b4c8e6..5d10949be36 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import classNames from 'classnames'; import { isEqual } from 'lodash'; -import PanelHeaderCorner from './PanelHeaderCorner'; +import { PanelHeaderCorner } from './PanelHeaderCorner'; import { PanelHeaderMenu } from './PanelHeaderMenu'; import templateSrv from 'app/features/templating/template_srv'; @@ -18,6 +18,8 @@ export interface Props { description?: string; scopedVars?: string; links?: []; + error?: string; + isFullscreen: boolean; } interface ClickCoordinates { @@ -68,10 +70,9 @@ export class PanelHeader extends Component { }; render() { - const isFullscreen = false; - const isLoading = false; + const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo, scopedVars } = this.props; const title = templateSrv.replaceWithText(panel.title, scopedVars); return ( @@ -82,13 +83,9 @@ export class PanelHeader extends Component { description={panel.description} scopedVars={panel.scopedVars} links={panel.links} + error={error} />
    - {isLoading && ( - - - - )}
    diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index e1c21315bd2..63ebed1632c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -6,7 +6,7 @@ import templateSrv from 'app/features/templating/template_srv'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -enum InfoModes { +enum InfoMode { Error = 'Error', Info = 'Info', Links = 'Links', @@ -18,18 +18,22 @@ interface Props { description?: string; scopedVars?: string; links?: []; + error?: string; } export class PanelHeaderCorner extends Component { timeSrv: TimeSrv = getTimeSrv(); getInfoMode = () => { - const { panel } = this.props; + const { panel, error } = this.props; + if (error) { + return InfoMode.Error; + } if (!!panel.description) { - return InfoModes.Info; + return InfoMode.Info; } if (panel.links && panel.links.length) { - return InfoModes.Links; + return InfoMode.Links; } return undefined; @@ -42,7 +46,7 @@ export class PanelHeaderCorner extends Component { const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars); const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown); - const html = ( + return (
    {panel.links && @@ -62,30 +66,35 @@ export class PanelHeaderCorner extends Component { )}
    ); - - return html; }; + renderCornerType(infoMode: InfoMode, content: string | JSX.Element) { + const theme = infoMode === InfoMode.Error ? 'error' : 'info'; + return ( + +
    + + +
    +
    + ); + } + render() { - const infoMode: InfoModes | undefined = this.getInfoMode(); + const infoMode: InfoMode | undefined = this.getInfoMode(); if (!infoMode) { return null; } - return ( - <> - {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? ( - -
    - - -
    -
    - ) : null} - - ); + if (infoMode === InfoMode.Error) { + return this.renderCornerType(infoMode, this.props.error); + } + + if (infoMode === InfoMode.Info) { + return this.renderCornerType(infoMode, this.getInfoContent()); + } + + return null; } } - -export default PanelHeaderCorner; diff --git a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx index 3f835bdbac2..4067f361f06 100644 --- a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx +++ b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; import { PanelPlugin } from 'app/types'; interface Props { @@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin { }, exports: { - Panel: NotFound, + reactPanel: new ReactPanelPlugin(NotFound), }, }; } diff --git a/public/app/features/dashboard/panel_editor/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx index d91737195f1..01a6e39cedb 100644 --- a/public/app/features/dashboard/panel_editor/GeneralTab.tsx +++ b/public/app/features/dashboard/panel_editor/GeneralTab.tsx @@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent { render() { return ( - +
    (this.element = element)} /> ); diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 74870b25f07..1bc42a2fd88 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -45,7 +45,7 @@ interface PanelEditorTab { const panelEditorTabTexts = { [PanelEditorTabIds.Queries]: 'Queries', [PanelEditorTabIds.Visualization]: 'Visualization', - [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Advanced]: 'General', [PanelEditorTabIds.Alert]: 'Alert', }; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index d46ff020906..bef23c03496 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent {
    {!isAddingMixed && ( - )} diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index cf89175a5ee..0b8d2c39908 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -28,28 +28,57 @@ interface State { loadedDataSourceValue: string | null | undefined; datasource: DataSourceApi | null; isCollapsed: boolean; - angularScope: AngularQueryComponentScope | null; + hasTextEditMode: boolean; } export class QueryEditorRow extends PureComponent { element: HTMLElement | null = null; + angularScope: AngularQueryComponentScope | null; angularQueryEditor: AngularComponent | null = null; state: State = { datasource: null, isCollapsed: false, - angularScope: null, loadedDataSourceValue: undefined, + hasTextEditMode: false, }; componentDidMount() { this.loadDatasource(); this.props.panel.events.on('refresh', this.onPanelRefresh); + this.props.panel.events.on('data-error', this.onPanelDataError); + this.props.panel.events.on('data-received', this.onPanelDataReceived); } + componentWillUnmount() { + this.props.panel.events.off('refresh', this.onPanelRefresh); + this.props.panel.events.off('data-error', this.onPanelDataError); + this.props.panel.events.off('data-received', this.onPanelDataReceived); + + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + } + } + + onPanelDataError = () => { + // Some query controllers listen to data error events and need a digest + if (this.angularQueryEditor) { + // for some reason this needs to be done in next tick + setTimeout(this.angularQueryEditor.digest); + } + }; + + onPanelDataReceived = () => { + // Some query controllers listen to data error events and need a digest + if (this.angularQueryEditor) { + // for some reason this needs to be done in next tick + setTimeout(this.angularQueryEditor.digest); + } + }; + onPanelRefresh = () => { - if (this.state.angularScope) { - this.state.angularScope.range = getTimeSrv().timeRange(); + if (this.angularScope) { + this.angularScope.range = getTimeSrv().timeRange(); } }; @@ -73,7 +102,11 @@ export class QueryEditorRow extends PureComponent { const dataSourceSrv = getDatasourceSrv(); const datasource = await dataSourceSrv.get(query.datasource || panel.datasource); - this.setState({ datasource, loadedDataSourceValue: this.props.dataSourceValue }); + this.setState({ + datasource, + loadedDataSourceValue: this.props.dataSourceValue, + hasTextEditMode: false, + }); } componentDidUpdate() { @@ -98,21 +131,14 @@ export class QueryEditorRow extends PureComponent { const scopeProps = { ctrl: this.getAngularQueryComponentScope() }; this.angularQueryEditor = loader.load(this.element, scopeProps, template); + this.angularScope = scopeProps.ctrl; // give angular time to compile setTimeout(() => { - this.setState({ angularScope: scopeProps.ctrl }); + this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode }); }, 10); } - componentWillUnmount() { - this.props.panel.events.off('refresh', this.onPanelRefresh); - - if (this.angularQueryEditor) { - this.angularQueryEditor.destroy(); - } - } - onToggleCollapse = () => { this.setState({ isCollapsed: !this.state.isCollapsed }); }; @@ -138,10 +164,8 @@ export class QueryEditorRow extends PureComponent { } onToggleEditMode = () => { - const { angularScope } = this.state; - - if (angularScope && angularScope.toggleEditorMode) { - angularScope.toggleEditorMode(); + if (this.angularScope && this.angularScope.toggleEditorMode) { + this.angularScope.toggleEditorMode(); this.angularQueryEditor.digest(); } @@ -150,11 +174,6 @@ export class QueryEditorRow extends PureComponent { } }; - get hasTextEditMode() { - const { angularScope } = this.state; - return angularScope && angularScope.toggleEditorMode; - } - onRemoveQuery = () => { this.props.onRemoveQuery(this.props.query); }; @@ -171,10 +190,8 @@ export class QueryEditorRow extends PureComponent { }; renderCollapsedText(): string | null { - const { angularScope } = this.state; - - if (angularScope && angularScope.getCollapsedText) { - return angularScope.getCollapsedText(); + if (this.angularScope && this.angularScope.getCollapsedText) { + return this.angularScope.getCollapsedText(); } return null; @@ -182,7 +199,7 @@ export class QueryEditorRow extends PureComponent { render() { const { query, inMixedMode } = this.props; - const { datasource, isCollapsed } = this.state; + const { datasource, isCollapsed, hasTextEditMode } = this.state; const isDisabled = query.hide; const bodyClasses = classNames('query-editor-row__body gf-form-query', { @@ -212,7 +229,7 @@ export class QueryEditorRow extends PureComponent { {isCollapsed &&
    {this.renderCollapsedText()}
    }
    - {this.hasTextEditMode && ( + {hasTextEditMode && (
    @@ -156,7 +154,7 @@ export class UnConnectedExploreToolbar extends PureComponent { splitted, title: 'Run Query', onClick: this.onRunQuery, - buttonClassName: 'navbar-button--primary', + buttonClassName: 'navbar-button--secondary', iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon', iconSide: IconSide.right, })} diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx similarity index 85% rename from public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx rename to public/app/plugins/panel/gauge/GaugeOptionsBox.tsx index 50e2a344a9b..b5d6acca806 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx @@ -1,9 +1,14 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui'; +// Components +import { Switch, PanelOptionsGroup } from '@grafana/ui'; + +// Types +import { FormField, PanelEditorProps } from '@grafana/ui'; import { GaugeOptions } from './types'; -export default class GaugeOptionsEditor extends PureComponent> { +export class GaugeOptionsBox extends PureComponent> { onToggleThresholdLabels = () => this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels }); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 5cb256ee1aa..e7e60a7c417 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -16,9 +16,10 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { const { panelData, width, height, onInterpolate, options } = this.props; + const { valueOptions } = options; - const prefix = onInterpolate(options.prefix); - const suffix = onInterpolate(options.suffix); + const prefix = onInterpolate(valueOptions.prefix); + const suffix = onInterpolate(valueOptions.suffix); let value: TimeSeriesValue; if (panelData.timeSeries) { @@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent { }); if (vmSeries[0]) { - value = vmSeries[0].stats[options.stat]; + value = vmSeries[0].stats[valueOptions.stat]; } else { value = null; } @@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent { {theme => ( )} diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx similarity index 52% rename from public/app/plugins/panel/gauge/GaugePanelOptions.tsx rename to public/app/plugins/panel/gauge/GaugePanelEditor.tsx index 84726ac88bf..63031f9d895 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { - PanelOptionsProps, + PanelEditorProps, ThresholdsEditor, Threshold, PanelOptionsGrid, @@ -8,29 +8,11 @@ import { ValueMapping, } from '@grafana/ui'; -import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import GaugeOptionsEditor from './GaugeOptionsEditor'; -import { GaugeOptions } from './types'; - -export const defaultProps = { - options: { - minValue: 0, - maxValue: 100, - prefix: '', - showThresholdMarkers: true, - showThresholdLabels: false, - suffix: '', - decimals: 0, - stat: 'avg', - unit: 'none', - valueMappings: [], - thresholds: [], - }, -}; - -export default class GaugePanelOptions extends PureComponent> { - static defaultProps = defaultProps; +import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; +import { GaugeOptionsBox } from './GaugeOptionsBox'; +import { GaugeOptions, SingleStatValueOptions } from './types'; +export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, @@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent + this.props.onChange({ + ...this.props.options, + valueOptions, + }); + render() { const { onChange, options } = this.props; return ( <> - - + + diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx similarity index 74% rename from public/app/plugins/panel/gauge/ValueOptions.tsx rename to public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index 1fdccadddf2..86c177bb5e5 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -1,7 +1,12 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Components import UnitPicker from 'app/core/components/Select/UnitPicker'; -import { GaugeOptions } from './types'; +import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Types +import { SingleStatValueOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -19,24 +24,40 @@ const statOptions = [ const labelWidth = 6; -export default class ValueOptions extends PureComponent> { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); +export interface Props { + options: SingleStatValueOptions; + onChange: (valueOptions: SingleStatValueOptions) => void; +} +export class SingleStatValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); onDecimalChange = event => { if (!isNaN(event.target.value)) { - this.props.onChange({ ...this.props.options, decimals: event.target.value }); + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); } }; onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); render() { const { stat, unit, decimals, prefix, suffix } = this.props.options; + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + return (
    @@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 783e4825657..a32cb7cd538 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,4 +1,10 @@ -import GaugePanelOptions, { defaultProps } from './GaugePanelOptions'; -import { GaugePanel } from './GaugePanel'; +import { ReactPanelPlugin } from '@grafana/ui'; -export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults }; +import { GaugePanelEditor } from './GaugePanelEditor'; +import { GaugePanel } from './GaugePanel'; +import { GaugeOptions, defaults } from './types'; + +export const reactPanel = new ReactPanelPlugin(GaugePanel); + +reactPanel.setEditor(GaugePanelEditor); +reactPanel.setDefaults(defaults); diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index 42262178dc8..10dd475eff5 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,15 +1,35 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - decimals: number; valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; showThresholdLabels: boolean; showThresholdMarkers: boolean; - stat: string; - suffix: string; thresholds: Threshold[]; - unit: string; + valueOptions: SingleStatValueOptions; } + +export interface SingleStatValueOptions { + unit: string; + suffix: string; + stat: string; + prefix: string; + decimals?: number | null; +} + +export const defaults: GaugeOptions = { + minValue: 0, + maxValue: 100, + showThresholdMarkers: true, + showThresholdLabels: false, + valueOptions: { + prefix: '', + suffix: '', + decimals: null, + stat: 'avg', + unit: 'none', + }, + valueMappings: [], + thresholds: [], +}; diff --git a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx similarity index 91% rename from public/app/plugins/panel/graph2/GraphPanelOptions.tsx rename to public/app/plugins/panel/graph2/GraphPanelEditor.tsx index a9c2d299589..80b17ccd5c4 100644 --- a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx +++ b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx @@ -3,10 +3,10 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelOptionsProps, Switch } from '@grafana/ui'; +import { PanelEditorProps, Switch } from '@grafana/ui'; import { Options } from './types'; -export class GraphPanelOptions extends PureComponent> { +export class GraphPanelEditor extends PureComponent> { onToggleLines = () => { this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines }); }; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 762d5609541..a3a3fadf6bf 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,4 +1,4 @@ import { GraphPanel } from './GraphPanel'; -import { GraphPanelOptions } from './GraphPanelOptions'; +import { GraphPanelEditor } from './GraphPanelEditor'; -export { GraphPanel as Panel, GraphPanelOptions as PanelOptions }; +export { GraphPanel as Panel, GraphPanelEditor as PanelOptions }; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index cc3ec016273..884a5927a19 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; export class Text2 extends PureComponent { constructor(props: PanelProps) { @@ -11,4 +11,4 @@ export class Text2 extends PureComponent { } } -export { Text2 as Panel }; +export const reactPanel = new ReactPanelPlugin(Text2); diff --git a/public/img/icons_dark_theme/icon_explore.svg b/public/img/icons_dark_theme/icon_explore.svg new file mode 100644 index 00000000000..8d7e1b93fce --- /dev/null +++ b/public/img/icons_dark_theme/icon_explore.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_explore.svg b/public/img/icons_light_theme/icon_explore.svg new file mode 100644 index 00000000000..8732b2d9ae8 --- /dev/null +++ b/public/img/icons_light_theme/icon_explore.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/public/sass/_variables.scss.d.ts b/public/sass/_variables.scss.d.ts index 5b44580a7aa..992dbb7bddf 100644 --- a/public/sass/_variables.scss.d.ts +++ b/public/sass/_variables.scss.d.ts @@ -1,6 +1,6 @@ export interface GrafanaVariables { - panelHorizontalPadding: number; - panelVerticalPadding: number; + panelhorizontalpadding: number; + panelverticalpadding: number; } declare const variables: GrafanaVariables; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index 2d0cb394da1..8d171673f6b 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -192,6 +192,10 @@ background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); } +.gicon-explore { + background-image: url('../img/icons_#{$theme-name}_theme/icon_explore.svg'); +} + .sidemenu { .gicon-dashboard { background-image: url('../img/icons_dark_theme/icon_dashboard.svg'); @@ -205,6 +209,9 @@ .gicon-question { background-image: url('../img/icons_dark_theme/icon_question.svg'); } + .gicon-explore { + background-image: url('../img/icons_dark_theme/icon_explore.svg'); + } } .fa--permissions-list { diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index a915d003eab..a86b8c450a0 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -1,9 +1,8 @@ .navbar { position: relative; - padding-left: 20px; z-index: $zindex-navbar-fixed; height: $navbarHeight; - padding-right: 20px; + padding: 0 20px 0 50px; display: flex; flex-grow: 1; border-bottom: 1px solid transparent; @@ -46,9 +45,8 @@ .navbar-button--add-panel, .navbar-button--star, - .navbar-button--tv, - .navbar-buttons--close { - display: flex; + .navbar-button--tv { + display: none; } } @@ -58,15 +56,14 @@ white-space: nowrap; display: block; margin: 0; - color: darken($link-color, 5%); + color: $headings-color; font-size: $font-size-lg; - padding-left: 1rem; min-height: $navbarHeight; line-height: $navbarHeight; .fa-caret-down { font-size: 60%; - padding-left: 0.2rem; + padding-left: 6px; } &--search { @@ -74,12 +71,12 @@ } .gicon { + top: -2px; position: relative; - top: -1px; - font-size: 19px; + font-size: 17px; line-height: 8px; opacity: 0.75; - margin-right: 13px; + margin-right: 10px; display: none; } @@ -124,7 +121,7 @@ height: 30px; color: $text-muted; border: 1px solid $navbar-button-border; - margin-right: 3px; + margin-left: 3px; white-space: nowrap; .gicon { @@ -153,19 +150,19 @@ } } - &--primary { - @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); + &--secondary { + @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); } } @include media-breakpoint-up(sm) { .navbar { - padding-left: 50px; + padding-left: 60px; } .sidemenu-open { .navbar { - padding-left: 15px; + padding-left: 25px; margin-left: 0; } } @@ -181,7 +178,7 @@ display: flex; height: $navbarHeight; align-items: center; - padding-left: 7px; + padding-right: 13px; } .navbar-edit__back-btn { diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 44941540598..f30bdb5c79e 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -253,7 +253,7 @@ li.sidemenu-org-switcher { } .sidemenu__logo_small_breakpoint { - padding: 16px 10px 26px; + padding: 14px 10px 26px 13px; display: flex; flex-direction: row; justify-content: space-between; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 151130a69e2..9b8f18e8681 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -30,12 +30,6 @@ padding: 0; margin-left: 0; } - - .explore-toolbar-header-title { - .navbar-page-btn { - padding-left: 0; - } - } } .explore-toolbar { @@ -44,7 +38,7 @@ flex-flow: row wrap; justify-content: flex-start; height: auto; - padding: 0px $dashboard-padding; + padding: 0px $dashboard-padding 0 25px; border-bottom: 1px solid #0000; transition-duration: 0.35s; transition-timing-function: ease-in-out; @@ -87,22 +81,9 @@ align-items: center; } -.explore-toolbar-header-title { - color: darken($link-color, 5%); - - .navbar-page-btn { - padding-left: $dashboard-padding; - } - - .fa { - font-size: 100%; - opacity: 0.75; - margin-right: 0.5em; - } -} - .explore-toolbar-header-close { margin-left: auto; + color: $text-color-weak; } .explore-toolbar-content { @@ -156,7 +137,6 @@ .sidemenu-open { .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: 0; } } @@ -164,7 +144,6 @@ .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } } @@ -185,7 +164,6 @@ .sidemenu-open { .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } } @@ -193,7 +171,6 @@ .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } }