diff --git a/.circleci/config.yml b/.circleci/config.yml index 236d5aec398..7f9c40bd968 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,7 +127,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -200,51 +200,51 @@ jobs: - dist/grafana* grafana-docker-master: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - run: cd packaging/docker && ./build-enterprise.sh "master" grafana-docker-pr: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}" grafana-docker-release: - docker: - - image: docker:stable-git - steps: - - checkout - - attach_workspace: - at: . - - setup_remote_docker - - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" + machine: + image: circleci/classic:201808-01 + steps: + - checkout + - attach_workspace: + at: . + - run: docker info + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz + - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" build-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -276,7 +276,7 @@ jobs: build-all-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 671740f7225..46b7381cba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas) * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik) * **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok) +* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach) ### Bug fixes * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) diff --git a/README.md b/README.md index 1ce4ffbe109..ff5da04f209 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,9 @@ GRAFANA_TEST_DB=postgres go test ./pkg/... If you have any idea for an improvement or found a bug, do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana -the kickass metrics & devops dashboard we all dream about! +the kickass metrics & devops dashboard we all dream about! + +Read the [contributing](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide then check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are easy and that we would like help with. ## Plugin development diff --git a/build.go b/build.go index 9d5216de1d0..4486cd3deb9 100644 --- a/build.go +++ b/build.go @@ -164,6 +164,8 @@ func makeLatestDistCopies() { "_amd64.deb": "dist/grafana_latest_amd64.deb", ".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm", ".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz", + ".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz", + ".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz", } for _, file := range files { diff --git a/devenv/docker/blocks/influxdb/influxdb.conf b/devenv/docker/blocks/influxdb/influxdb.conf index c0331ce7449..120739dd896 100644 --- a/devenv/docker/blocks/influxdb/influxdb.conf +++ b/devenv/docker/blocks/influxdb/influxdb.conf @@ -69,6 +69,7 @@ reporting-disabled = false unix-socket-enabled = false # enable http service over unix domain socket # bind-socket = "/var/run/influxdb.sock" + flux-enabled = true [subscriber] enabled = true diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 6be12600da5..3d96923bc72 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized "list": [] }, "refresh": "5s", - "schemaVersion": 16, + "schemaVersion": 17, "version": 0, "links": [] } diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index f20cc0ccfc9..71ce6bdd2ae 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged. By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated -panel. Each panel will never be smaller that the provided `Min width` if you have many selected values. +panel. -By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. +Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything. + +By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build. You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard. diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6548f75e91f..91695dc5647 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -23,7 +23,10 @@ "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-transition-group": "^2.2.1", - "react-virtualized": "^9.21.0" + "react-virtualized": "^9.21.0", + "tether": "^1.4.0", + "tether-drop": "https://github.com/torkelo/drop/tarball/master", + "tinycolor2": "^1.4.1" }, "devDependencies": { "@types/classnames": "^2.2.6", @@ -33,6 +36,8 @@ "@types/react": "^16.7.6", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-test-renderer": "^16.0.3", + "@types/tether-drop": "^1.4.8", + "@types/tinycolor2": "^1.4.1", "react-test-renderer": "^16.7.0", "typescript": "^3.2.2" } diff --git a/public/app/core/specs/ColorPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx similarity index 80% rename from public/app/core/specs/ColorPalette.test.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx index fb1124aa975..0714180de54 100644 --- a/public/app/core/specs/ColorPalette.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import { ColorPalette } from '../components/colorpicker/ColorPalette'; +import { ColorPalette } from './ColorPalette'; describe('CollorPalette', () => { it('renders correctly', () => { diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx similarity index 90% rename from public/app/core/components/colorpicker/ColorPalette.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx index edb2629d16d..03ed9949361 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { sortedColors } from 'app/core/utils/colors'; +import { sortedColors } from '../../utils'; export interface Props { color: string; @@ -9,13 +9,13 @@ export interface Props { export class ColorPalette extends React.Component { paletteColors: string[]; - constructor(props) { + constructor(props: Props) { super(props); this.paletteColors = sortedColors; this.onColorSelect = this.onColorSelect.bind(this); } - onColorSelect(color) { + onColorSelect(color: string) { return () => { this.props.onColorSelect(color); }; diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 9541001b0a8..485aa5f03d3 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Drop from 'tether-drop'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface Props { color: string; @@ -10,7 +9,7 @@ export interface Props { } export class ColorPicker extends React.Component { - pickerElem: HTMLElement; + pickerElem: HTMLElement | null; colorPickerDrop: any; openColorPicker = () => { @@ -20,7 +19,7 @@ export class ColorPicker extends React.Component { ReactDOM.render(dropContent, dropContentElem); const drop = new Drop({ - target: this.pickerElem, + target: this.pickerElem as Element, content: dropContentElem, position: 'top center', classes: 'drop-popover', @@ -28,6 +27,7 @@ export class ColorPicker extends React.Component { hoverCloseDelay: 200, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); @@ -45,7 +45,7 @@ export class ColorPicker extends React.Component { }, 100); }; - onColorSelect = color => { + onColorSelect = (color: string) => { this.props.onChange(color); }; @@ -59,8 +59,3 @@ export class ColorPicker extends React.Component { ); } } - -react2AngularDirective('colorPicker', ColorPicker, [ - 'color', - ['onChange', { watchDepth: 'reference', wrapApply: true }], -]); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index c42bcfa1d06..e8305c99319 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -14,7 +14,7 @@ export interface Props { export class ColorPickerPopover extends React.Component { pickerNavElem: any; - constructor(props) { + constructor(props: Props) { super(props); this.state = { tab: 'palette', @@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component { }; } - setPickerNavElem(elem) { + setPickerNavElem(elem: any) { this.pickerNavElem = $(elem); } - setColor(color) { + setColor(color: string) { const newColor = tinycolor(color); if (newColor.isValid()) { - this.setState({ - color: newColor.toString(), - colorString: newColor.toString(), - }); + this.setState({ color: newColor.toString(), colorString: newColor.toString() }); this.props.onColorSelect(color); } } - sampleColorSelected(color) { + sampleColorSelected(color: string) { this.setColor(color); } - spectrumColorSelected(color) { + spectrumColorSelected(color: any) { const rgbColor = color.toRgbString(); this.setColor(rgbColor); } - onColorStringChange(e) { + onColorStringChange(e: any) { const colorString = e.target.value; - this.setState({ - colorString: colorString, - }); + this.setState({ colorString: colorString }); const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state const newColorString = newColor.toString(); - this.setState({ - color: newColorString, - }); + this.setState({ color: newColorString }); this.props.onColorSelect(newColorString); } } - onColorStringBlur(e) { + onColorStringBlur(e: any) { const colorString = e.target.value; this.setColor(colorString); } componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', e => { + this.pickerNavElem.on('show', (e: any) => { // use href attr (#name => name) const tab = e.target.hash.slice(1); - this.setState({ - tab: tab, - }); + this.setState({ tab: tab }); }); } diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx similarity index 96% rename from public/app/core/components/colorpicker/SeriesColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx index 32b7554e38d..7c3848f6868 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx @@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component { onToggleAxis: () => {}, }; - constructor(props) { + constructor(props: SeriesColorPickerProps) { super(props); } @@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component { remove: true, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); diff --git a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx similarity index 85% rename from public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 085d554300d..541a77ddabc 100644 --- a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface SeriesColorPickerPopoverProps { color: string; @@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent void; + onToggleAxis?: () => void; } interface AxisSelectorState { @@ -30,7 +29,7 @@ interface AxisSelectorState { } export class AxisSelector extends React.PureComponent { - constructor(props) { + constructor(props: AxisSelectorProps) { super(props); this.state = { yaxis: this.props.yaxis, @@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent { elem: any; isMoving: boolean; - constructor(props) { + constructor(props: Props) { super(props); this.onSpectrumMove = this.onSpectrumMove.bind(this); this.setComponentElem = this.setComponentElem.bind(this); } - setComponentElem(elem) { + setComponentElem(elem: any) { this.elem = $(elem); } - onSpectrumMove(color) { + onSpectrumMove(color: any) { this.isMoving = true; this.props.onColorSelect(color); } @@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component { this.elem.spectrum('set', this.props.color); } - componentWillUpdate(nextProps) { + componentWillUpdate(nextProps: any) { // If user move pointer over spectrum field this produce 'move' event and component // may update props.color. We don't want to update spectrum color in this case, so we can use // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which diff --git a/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap b/packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap similarity index 100% rename from public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap rename to packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap diff --git a/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx new file mode 100644 index 00000000000..01048014f8a --- /dev/null +++ b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx @@ -0,0 +1,11 @@ +import React, { SFC } from 'react'; + +interface LoadingPlaceholderProps { + text: string; +} + +export const LoadingPlaceholder: SFC = ({ text }) => ( +
+ {text} +
+); diff --git a/public/app/core/components/Select/IndicatorsContainer.tsx b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx similarity index 73% rename from public/app/core/components/Select/IndicatorsContainer.tsx rename to packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx index d4de51a2cef..260fe6ebbdf 100644 --- a/public/app/core/components/Select/IndicatorsContainer.tsx +++ b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx @@ -1,7 +1,10 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; -export const IndicatorsContainer = props => { +export const IndicatorsContainer = (props: any) => { const isOpen = props.selectProps.menuIsOpen; return ( diff --git a/public/app/core/components/Select/NoOptionsMessage.tsx b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx similarity index 83% rename from public/app/core/components/Select/NoOptionsMessage.tsx rename to packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx index 5fe229340a4..1cec06a5301 100644 --- a/public/app/core/components/Select/NoOptionsMessage.tsx +++ b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx @@ -1,5 +1,9 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; +// @ts-ignore import { OptionProps } from '@torkelo/react-select/lib/components/Option'; export interface Props { diff --git a/public/app/core/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx similarity index 92% rename from public/app/core/components/Select/Select.tsx rename to packages/grafana-ui/src/components/Select/Select.tsx index f66e07c9ed6..b3b0c8efbbb 100644 --- a/public/app/core/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -1,16 +1,21 @@ // Libraries import classNames from 'classnames'; import React, { PureComponent } from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { default as ReactSelect } from '@torkelo/react-select'; +// @ts-ignore import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; +// @ts-ignore import { components } from '@torkelo/react-select'; // Components -import { Option, SingleValue } from './PickerOption'; -import OptionGroup from './OptionGroup'; +import { SelectOption, SingleValue } from './SelectOption'; +import SelectOptionGroup from './SelectOptionGroup'; import IndicatorsContainer from './IndicatorsContainer'; import NoOptionsMessage from './NoOptionsMessage'; -import ResetStyles from './ResetStyles'; +import resetSelectStyles from './resetSelectStyles'; import { CustomScrollbar } from '@grafana/ui'; export interface SelectOptionItem { @@ -53,7 +58,7 @@ interface AsyncProps { loadingMessage?: () => string; } -export const MenuList = props => { +export const MenuList = (props: any) => { return ( {props.children} @@ -112,11 +117,11 @@ export class Select extends PureComponent { classNamePrefix="gf-form-select-box" className={selectClassNames} components={{ - Option, + Option: SelectOption, SingleValue, IndicatorsContainer, MenuList, - Group: OptionGroup, + Group: SelectOptionGroup, }} defaultValue={defaultValue} value={value} @@ -127,7 +132,7 @@ export class Select extends PureComponent { onChange={onChange} options={options} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} isDisabled={isDisabled} isLoading={isLoading} isClearable={isClearable} @@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent { isLoading={isLoading} defaultOptions={defaultOptions} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} loadingMessage={loadingMessage} noOptionsMessage={noOptionsMessage} isDisabled={isDisabled} diff --git a/public/app/core/components/Select/PickerOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx similarity index 52% rename from public/app/core/components/Select/PickerOption.test.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.test.tsx index 6b4aedcfcc0..a7326b3f4db 100644 --- a/public/app/core/components/Select/PickerOption.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import PickerOption from './PickerOption'; +import SelectOption from './SelectOption'; +import { OptionProps } from 'react-select/lib/components/Option'; -const model = { +const model: OptionProps = { cx: jest.fn(), clearValue: jest.fn(), - onSelect: jest.fn(), getStyles: jest.fn(), getValue: jest.fn(), hasValue: true, @@ -18,21 +18,31 @@ const model = { isFocused: false, isSelected: false, innerRef: null, - innerProps: null, - label: 'Option label', - type: null, - children: 'Model title', - data: { - title: 'Model title', - imgUrl: 'url/to/avatar', - label: 'User picker label', + innerProps: { + id: '', + key: '', + onClick: jest.fn(), + onMouseOver: jest.fn(), + tabIndex: 1, }, + label: 'Option label', + type: 'option', + children: 'Model title', className: 'class-for-user-picker', }; -describe('PickerOption', () => { +describe('SelectOption', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const tree = renderer + .create( + + ) + .toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/Select/PickerOption.tsx b/packages/grafana-ui/src/components/Select/SelectOption.tsx similarity index 84% rename from public/app/core/components/Select/PickerOption.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.tsx index d263f6f832b..5f94c2182f9 100644 --- a/public/app/core/components/Select/PickerOption.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.tsx @@ -1,4 +1,7 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; import { OptionProps } from 'react-select/lib/components/Option'; @@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps { }; } -export const Option = (props: ExtendedOptionProps) => { +export const SelectOption = (props: ExtendedOptionProps) => { const { children, isSelected, data } = props; return ( @@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => { }; // was not able to type this without typescript error -export const SingleValue = props => { +export const SingleValue = (props: any) => { const { children, data } = props; return ( @@ -41,4 +44,4 @@ export const SingleValue = props => { ); }; -export default Option; +export default SelectOption; diff --git a/public/app/core/components/Select/OptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx similarity index 89% rename from public/app/core/components/Select/OptionGroup.tsx rename to packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index a001f58c681..30842f02e29 100644 --- a/public/app/core/components/Select/OptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -9,7 +9,7 @@ interface State { expanded: boolean; } -export default class OptionGroup extends PureComponent { +export default class SelectOptionGroup extends PureComponent { state = { expanded: false, }; @@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent +exports[`SelectOption renders correctly 1`] = ` +
diff --git a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts new file mode 100644 index 00000000000..a980741c17c --- /dev/null +++ b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts @@ -0,0 +1,27 @@ +export default function resetSelectStyles() { + return { + clearIndicator: () => ({}), + container: () => ({}), + control: () => ({}), + dropdownIndicator: () => ({}), + group: () => ({}), + groupHeading: () => ({}), + indicatorsContainer: () => ({}), + indicatorSeparator: () => ({}), + input: () => ({}), + loadingIndicator: () => ({}), + loadingMessage: () => ({}), + menu: () => ({}), + menuList: ({ maxHeight }: { maxHeight: number }) => ({ + maxHeight, + }), + multiValue: () => ({}), + multiValueLabel: () => ({}), + multiValueRemove: () => ({}), + noOptionsMessage: () => ({}), + option: () => ({}), + placeholder: () => ({}), + singleValue: () => ({}), + valueContainer: () => ({}), + }; +} diff --git a/public/app/plugins/panel/gauge/Threshold.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx similarity index 72% rename from public/app/plugins/panel/gauge/Threshold.test.tsx rename to packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 852b9f4c104..14f84e00f80 100644 --- a/public/app/plugins/panel/gauge/Threshold.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,23 +1,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import Thresholds from './Thresholds'; -import { defaultProps } from './GaugePanelOptions'; -import { BasicGaugeColor } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; + +import { ThresholdsEditor, Props } from './ThresholdsEditor'; +import { BasicGaugeColor } from '../../types'; const setup = (propOverrides?: object) => { - const props: PanelOptionsProps = { + const props: Props = { onChange: jest.fn(), - options: { - ...defaultProps.options, - thresholds: [], - }, + thresholds: [], }; Object.assign(props, propOverrides); - return shallow().instance() as Thresholds; + return shallow().instance() as ThresholdsEditor; }; describe('Add threshold', () => { @@ -31,10 +26,7 @@ describe('Add threshold', () => { it('should add another threshold above a first', () => { const instance = setup({ - options: { - ...defaultProps.options, - thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }], - }, + thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }], }); instance.onAddThreshold(1); diff --git a/public/app/plugins/panel/gauge/Thresholds.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx similarity index 68% rename from public/app/plugins/panel/gauge/Thresholds.tsx rename to packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index b4d4930e11d..54165dfadb5 100644 --- a/public/app/plugins/panel/gauge/Thresholds.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,32 +1,37 @@ import React, { PureComponent } from 'react'; -import tinycolor from 'tinycolor2'; -import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker'; -import { BasicGaugeColor, Threshold } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; +import tinycolor, { ColorInput } from 'tinycolor2'; + +import { Threshold, BasicGaugeColor } from '../../types'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; + +export interface Props { + thresholds: Threshold[]; + onChange: (thresholds: Threshold[]) => void; +} interface State { thresholds: Threshold[]; baseColor: string; } -export default class Thresholds extends PureComponent, State> { - constructor(props) { +export class ThresholdsEditor extends PureComponent { + constructor(props: Props) { super(props); - this.state = { - thresholds: props.options.thresholds, - baseColor: props.options.baseColor, - }; + this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green }; } - onAddThreshold = index => { - const { maxValue, minValue } = this.props.options; + onAddThreshold = (index: number) => { + const maxValue = 100; // hardcoded for now before we add the base threshold + const minValue = 0; // hardcoded for now before we add the base threshold const { thresholds } = this.state; const newThresholds = thresholds.map(threshold => { if (threshold.index >= index) { - threshold = { ...threshold, index: threshold.index + 1 }; + threshold = { + ...threshold, + index: threshold.index + 1, + }; } return threshold; @@ -48,27 +53,32 @@ export default class Thresholds extends PureComponent if (index === 0 && thresholds.length === 0) { color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString(); } else { - color = tinycolor.mix(thresholds[index - 1].color, BasicGaugeColor.Red, 50).toRgbString(); + color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString(); } this.setState( { - thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]), + thresholds: this.sortThresholds([ + ...newThresholds, + { + index, + value: value as number, + color, + }, + ]), }, () => this.updateGauge() ); }; - onRemoveThreshold = threshold => { + onRemoveThreshold = (threshold: Threshold) => { this.setState( - prevState => ({ - thresholds: prevState.thresholds.filter(t => t !== threshold), - }), + prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }), () => this.updateGauge() ); }; - onChangeThresholdValue = (event, threshold) => { + onChangeThresholdValue = (event: any, threshold: Threshold) => { const { thresholds } = this.state; const newThresholds = thresholds.map(t => { @@ -79,12 +89,10 @@ export default class Thresholds extends PureComponent return t; }); - this.setState({ - thresholds: newThresholds, - }); + this.setState({ thresholds: newThresholds }); }; - onChangeThresholdColor = (threshold, color) => { + onChangeThresholdColor = (threshold: Threshold, color: string) => { const { thresholds } = this.state; const newThresholds = thresholds.map(t => { @@ -103,20 +111,18 @@ export default class Thresholds extends PureComponent ); }; - onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color }); + onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); onBlur = () => { - this.setState(prevState => ({ - thresholds: this.sortThresholds(prevState.thresholds), - })); + this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) })); this.updateGauge(); }; updateGauge = () => { - this.props.onChange({ ...this.props.options, thresholds: this.state.thresholds }); + this.props.onChange(this.state.thresholds); }; - sortThresholds = thresholds => { + sortThresholds = (thresholds: Threshold[]) => { return thresholds.sort((t1, t2) => { return t2.value - t1.value; }); @@ -161,20 +167,8 @@ export default class Thresholds extends PureComponent return thresholds.map((t, i) => { return (
-
this.onAddThreshold(t.index + 1)} - style={{ - height: '50%', - backgroundColor: t.color, - }} - /> -
this.onAddThreshold(t.index)} - style={{ - height: '50%', - backgroundColor: t.color, - }} - /> +
this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} /> +
this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
); }); @@ -185,14 +179,14 @@ export default class Thresholds extends PureComponent
this.onAddThreshold(0)} - style={{ height: '100%', backgroundColor: this.props.options.baseColor }} + style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }} />
); } renderBase() { - const { baseColor } = this.props.options; + const baseColor = BasicGaugeColor.Green; return (
diff --git a/public/sass/components/_thresholds.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss similarity index 100% rename from public/sass/components/_thresholds.scss rename to packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index e1d1474bb16..c2fe032d24e 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1,3 +1,5 @@ @import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; +@import 'ThresholdsEditor/ThresholdsEditor'; @import 'Tooltip/Tooltip'; +@import 'Select/Select'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index abb1cf1b34c..936e1a1c759 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -2,3 +2,15 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; export { Tooltip } from './Tooltip/Tooltip'; export { Portal } from './Portal/Portal'; export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; + +// Select +export { Select, AsyncSelect, SelectOptionItem } from './Select/Select'; +export { IndicatorsContainer } from './Select/IndicatorsContainer'; +export { NoOptionsMessage } from './Select/NoOptionsMessage'; +export { default as resetSelectStyles } from './Select/resetSelectStyles'; + +export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; +export { ColorPicker } from './ColorPicker/ColorPicker'; +export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; +export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; +export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; diff --git a/public/app/plugins/panel/gauge/types.ts b/packages/grafana-ui/src/types/gauge.ts similarity index 76% rename from public/app/plugins/panel/gauge/types.ts rename to packages/grafana-ui/src/types/gauge.ts index 60c4fd1581d..e05849448f7 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/packages/grafana-ui/src/types/gauge.ts @@ -1,6 +1,6 @@ -import { RangeMap, ValueMap, Threshold } from 'app/types'; +import { RangeMap, Threshold, ValueMap } from './panel'; -export interface Options { +export interface GaugeOptions { baseColor: string; decimals: number; mappings: Array; diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index f618ce6db34..814ab0478db 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -1,3 +1,4 @@ export * from './series'; export * from './time'; export * from './panel'; +export * from './gauge'; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 44336555a81..0b995f932f0 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -29,3 +29,35 @@ export interface PanelMenuItem { shortcut?: string; subMenu?: PanelMenuItem[]; } + +export interface Threshold { + index: number; + value: number; + color?: string; +} + +export enum BasicGaugeColor { + Green = '#299c46', + Red = '#d44a3a', +} + +export enum MappingType { + ValueToText = 1, + RangeToText = 2, +} + +interface BaseMap { + id: number; + operator: string; + text: string; + type: MappingType; +} + +export interface ValueMap extends BaseMap { + value: string; +} + +export interface RangeMap extends BaseMap { + from: string; + to: string; +} diff --git a/packages/grafana-ui/src/utils/colors.ts b/packages/grafana-ui/src/utils/colors.ts new file mode 100644 index 00000000000..263d128aec4 --- /dev/null +++ b/packages/grafana-ui/src/utils/colors.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; +export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; +export const OK_COLOR = 'rgba(11, 237, 50, 1)'; +export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; +export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; +export const REGION_FILL_ALPHA = 0.09; + +export const colors = [ + '#7EB26D', // 0: pale green + '#EAB839', // 1: mustard + '#6ED0E0', // 2: light blue + '#EF843C', // 3: orange + '#E24D42', // 4: red + '#1F78C1', // 5: ocean + '#BA43A9', // 6: purple + '#705DA0', // 7: violet + '#508642', // 8: dark green + '#CCA300', // 9: dark sand + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +function sortColorsByHue(hexColors: string[]) { + const hslColors = _.map(hexColors, hexToHsl); + + const sortedHSLColors = _.sortBy(hslColors, ['h']); + const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors)); + + return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex); +} + +function hexToHsl(color: string) { + return tinycolor(color).toHsl(); +} + +function hslToHex(color: any) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 77940e19719..aeb65032067 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './processTimeSeries'; export * from './valueFormats/valueFormats'; +export * from './colors'; diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 4d4f6539972..d4f2f2aa7a3 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz" @@ -10,7 +11,8 @@ COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GF_UID="472" ARG GF_GID="472" diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh index ac3226a4a61..22655bead8c 100755 --- a/packaging/docker/build-deploy.sh +++ b/packaging/docker/build-deploy.sh @@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" ./push_to_docker_hub.sh "$_grafana_version" if echo "$_grafana_version" | grep -q "^master-"; then - apk add --no-cache curl ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version" fi diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index c303c71cd5f..a522363089b 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -1,25 +1,49 @@ #!/bin/sh -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi echo "Building ${_docker_repo}:${_grafana_version}" -docker build \ - --tag "${_docker_repo}:${_grafana_version}" \ - --no-cache=true . +export DOCKER_CLI_EXPERIMENTAL=enabled + +# Build grafana image for a specific arch +docker_build () { + base_image=$1 + grafana_tgz=$2 + tag=$3 + + docker build \ + --build-arg BASE_IMAGE=${base_image} \ + --build-arg GRAFANA_TGZ=${grafana_tgz} \ + --tag "${tag}" \ + --no-cache=true . +} + +# Tag docker images of all architectures +docker_tag_all () { + repo=$1 + tag=$2 + docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}" + docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}" + docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}" +} + +docker_build "debian:stretch-slim" "grafana-latest.linux-x64.tar.gz" "${_docker_repo}:${_grafana_version}" +docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}" +docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}" # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master if echo "$_grafana_tag" | grep -q "^v"; then - docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest" + docker_tag_all "${_docker_repo}" "latest" else - docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master" + docker_tag_all "${_docker_repo}" "master" + docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana-dev:${_grafana_version}" fi diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh index 526c216f8fa..37b5ae0095c 100755 --- a/packaging/docker/push_to_docker_hub.sh +++ b/packaging/docker/push_to_docker_hub.sh @@ -1,24 +1,46 @@ #!/bin/sh set -e -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi +export DOCKER_CLI_EXPERIMENTAL=enabled + echo "pushing ${_docker_repo}:${_grafana_version}" -docker push "${_docker_repo}:${_grafana_version}" + + +docker_push_all () { + repo=$1 + tag=$2 + + # Push each image individually + docker push "${repo}:${tag}" + docker push "${repo}-arm32v7-linux:${tag}" + docker push "${repo}-arm64v8-linux:${tag}" + + # Create and push a multi-arch manifest + docker manifest create "${repo}:${tag}" \ + "${repo}:${tag}" \ + "${repo}-arm32v7-linux:${tag}" \ + "${repo}-arm64v8-linux:${tag}" + + docker manifest push "${repo}:${tag}" +} if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then echo "pushing ${_docker_repo}:latest" - docker push "${_docker_repo}:latest" + docker_push_all "${_docker_repo}" "latest" + docker_push_all "${_docker_repo}" "${_grafana_version}" +elif echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -q "beta"; then + docker_push_all "${_docker_repo}" "${_grafana_version}" elif echo "$_grafana_tag" | grep -q "master"; then - echo "pushing grafana/grafana:master" - docker push grafana/grafana:master + docker_push_all "${_docker_repo}" "master" + docker push "grafana/grafana-dev:${_grafana_version}" fi diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3a8010e797b..0f3f56175fe 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard { func NewDashboardFolder(title string) *Dashboard { folder := NewDashboard(title) folder.IsFolder = true - folder.Data.Set("schemaVersion", 16) + folder.Data.Set("schemaVersion", 17) folder.Data.Set("version", 0) folder.IsFolder = true return folder diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e33e3dc2af3..5b911c5a9ad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) if err != nil { - return nil, ValidationError{Reason: "Could not parse frequency"} + return nil, ValidationError{Reason: err.Error()} } rawFor := jsonAlert.Get("for").MustString() diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index d2a505145ac..4423046d600 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -1,16 +1,21 @@ package alerting import ( + "errors" "fmt" "regexp" "strconv" "time" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" ) +var ( + ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`) + ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`) +) + type Rule struct { Id int64 OrgId int64 @@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { matches := ValueFormatRegex.FindAllString(str, 1) if len(matches) <= 0 { - return 0, fmt.Errorf("Frequency could not be parsed") + return 0, ErrFrequencyCouldNotBeParsed } value, err := strconv.Atoi(matches[0]) @@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { return 0, err } + if value == 0 { + return 0, ErrFrequencyCannotBeZeroOrLess + } + unit := UnitFormatRegex.FindAllString(str, 1)[0] if val, ok := unitMultiplier[unit]; ok { @@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.PanelId = ruleDef.PanelId model.Name = ruleDef.Name model.Message = ruleDef.Message - model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate model.For = ruleDef.For @@ -109,6 +117,13 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges + model.Frequency = ruleDef.Frequency + // frequency cannot be zero since that would not execute the alert rule. + // so we fallback to 60 seconds if `Freqency` is missing + if model.Frequency == 0 { + model.Frequency = 60 + } + for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) id, err := jsonModel.Get("id").Int64() diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 2a9e95e5723..cf25cc118f4 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) { return &ConditionResult{}, nil } +func TestAlertRuleFrequencyParsing(t *testing.T) { + tcs := []struct { + input string + err error + result int64 + }{ + {input: "10s", result: 10}, + {input: "10m", result: 600}, + {input: "1h", result: 3600}, + {input: "1o", result: 1}, + {input: "0s", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0m", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0h", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "-1s", err: ErrFrequencyCouldNotBeParsed}, + } + + for _, tc := range tcs { + r, err := getTimeDurationStringToSeconds(tc.input) + if err != tc.err { + t.Errorf("expected error: '%v' got: '%v'", tc.err, err) + return + } + + if r != tc.result { + t.Errorf("expected result: %d got %d", tc.result, r) + } + } +} + func TestAlertRuleModel(t *testing.T) { Convey("Testing alert rule", t, func() { @@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) { return &FakeCondition{}, nil }) - Convey("Can parse seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("10s") - So(seconds, ShouldEqual, 10) - }) - - Convey("Can parse minutes", func() { - seconds, _ := getTimeDurationStringToSeconds("10m") - So(seconds, ShouldEqual, 600) - }) - - Convey("Can parse hours", func() { - seconds, _ := getTimeDurationStringToSeconds("1h") - So(seconds, ShouldEqual, 3600) - }) - - Convey("defaults to seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("1o") - So(seconds, ShouldEqual, 1) - }) - Convey("should return err for empty string", func() { _, err := getTimeDurationStringToSeconds("") So(err, ShouldNotBeNil) @@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) { So(len(alertRule.Notifications), ShouldEqual, 2) }) }) + + Convey("can construct alert rule model with invalid frequency", func() { + json := ` + { + "name": "name2", + "description": "desc2", + "noDataMode": "critical", + "enabled": true, + "frequency": "0s", + "conditions": [ { "type": "test", "prop": 123 } ], + "notifications": [] + }` + + alertJSON, jsonErr := simplejson.NewJson([]byte(json)) + So(jsonErr, ShouldBeNil) + + alert := &m.Alert{ + Id: 1, + OrgId: 1, + DashboardId: 1, + PanelId: 1, + Frequency: 0, + + Settings: alertJSON, + } + + alertRule, err := NewRuleFromDBAlert(alert) + So(err, ShouldBeNil) + So(alertRule.Frequency, ShouldEqual, 60) + }) }) } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 5609c058a27..d6fc68293c3 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -6,6 +6,7 @@ import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; import AppNotificationList from './components/AppNotifications/AppNotificationList'; +import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +20,13 @@ export function registerAngularDirectives() { ['onChange', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); + react2AngularDirective('colorPicker', ColorPicker, [ + 'color', + ['onChange', { watchDepth: 'reference', wrapApply: true }], + ]); + react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + 'series', + 'onColorChange', + 'onToggleAxis', + ]); } diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 749bef680bf..30219371257 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker'; -import { Select, SelectOptionItem } from 'app/core/components/Select/Select'; +import { Select, SelectOptionItem } from '@grafana/ui'; import { User } from 'app/types'; import { dashboardPermissionLevels, diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx index d3f9ddbb1fb..ebf3cbad1bc 100644 --- a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import Select from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels } from 'app/types/acl'; export interface Props { diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index e726667cfbb..c33b564154a 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { Select } from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; import { FolderInfo } from 'app/types'; diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index 1a9081038c0..372c4cd4013 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import Select from './Select'; +import { Select } from '@grafana/ui'; // Types import { DataSourceSelectItem } from 'app/types'; diff --git a/public/app/core/components/Select/ResetStyles.tsx b/public/app/core/components/Select/ResetStyles.tsx deleted file mode 100644 index c34abb544ab..00000000000 --- a/public/app/core/components/Select/ResetStyles.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default { - clearIndicator: () => ({}), - container: () => ({}), - control: () => ({}), - dropdownIndicator: () => ({}), - group: () => ({}), - groupHeading: () => ({}), - indicatorsContainer: () => ({}), - indicatorSeparator: () => ({}), - input: () => ({}), - loadingIndicator: () => ({}), - loadingMessage: () => ({}), - menu: () => ({}), - menuList: ({ maxHeight }: { maxHeight: number }) => ({ - maxHeight, - }), - multiValue: () => ({}), - multiValueLabel: () => ({}), - multiValueRemove: () => ({}), - noOptionsMessage: () => ({}), - option: () => ({}), - placeholder: () => ({}), - singleValue: () => ({}), - valueContainer: () => ({}), -}; diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index bc608318806..8d9e1d48d81 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import _ from 'lodash'; -import { AsyncSelect } from './Select'; +import { AsyncSelect } from '@grafana/ui'; import { debounce } from 'lodash'; import { getBackendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/core/components/Select/UnitPicker.tsx b/public/app/core/components/Select/UnitPicker.tsx index 0da8a148b9a..f9dbc0ae421 100644 --- a/public/app/core/components/Select/UnitPicker.tsx +++ b/public/app/core/components/Select/UnitPicker.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import Select from './Select'; import { getValueFormats } from '@grafana/ui'; +import { Select } from '@grafana/ui'; interface Props { onChange: (item: any) => void; diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 8496d707105..ff4ae32f068 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; // Components -import { AsyncSelect } from './Select'; +import { AsyncSelect } from '@grafana/ui'; // Utils & Services import { debounce } from 'lodash'; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index d41626d9a2f..b13393ab2e1 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { Label } from 'app/core/components/Label/Label'; -import Select from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit } from 'app/types'; diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 4b2de6b1b16..7e8bc9c6fd2 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -1,12 +1,10 @@ import React from 'react'; +import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui'; import AsyncSelect from '@torkelo/react-select/lib/Async'; import { TagOption } from './TagOption'; import { TagBadge } from './TagBadge'; -import IndicatorsContainer from 'app/core/components/Select/IndicatorsContainer'; -import NoOptionsMessage from 'app/core/components/Select/NoOptionsMessage'; import { components } from '@torkelo/react-select'; -import ResetStyles from 'app/core/components/Select/ResetStyles'; export interface Props { tags: string[]; @@ -51,7 +49,7 @@ export class TagFilter extends React.Component { getOptionValue: i => i.value, getOptionLabel: i => i.label, value: tags, - styles: ResetStyles, + styles: resetSelectStyles(), filterOption: (option, searchQuery) => { const regex = RegExp(searchQuery, 'i'); return regex.test(option.value); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 257a2077c97..6713d8bcd14 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -13,11 +13,10 @@ import './partials'; import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; -import './components/colorpicker/ColorPicker'; -import './components/colorpicker/SeriesColorPickerPopover'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; +import { colors } from '@grafana/ui/'; import { searchDirective } from './components/search/search'; import { infoPopover } from './components/info_popover'; @@ -36,7 +35,6 @@ import 'app/core/services/all'; import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; -import colors from './utils/colors'; import { assignModelProperties } from './utils/model_utils'; import { contextSrv } from './services/context_srv'; import { KeybindingSrv } from './services/keybindingSrv'; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4e8c6207959..4cf9a029a2a 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,6 +1,8 @@ import _ from 'lodash'; +import { colors } from '@grafana/ui'; + import { TimeSeries } from 'app/core/core'; -import colors, { getThemeColor } from 'app/core/utils/colors'; +import { getThemeColor } from 'app/core/utils/colors'; /** * Mapping of log level abbreviation to canonical log level. diff --git a/public/app/core/specs/factors.test.ts b/public/app/core/specs/factors.test.ts new file mode 100644 index 00000000000..aed59b5be8b --- /dev/null +++ b/public/app/core/specs/factors.test.ts @@ -0,0 +1,8 @@ +import getFactors from 'app/core/utils/factors'; + +describe('factors', () => { + it('should return factors for 12', () => { + const factors = getFactors(12); + expect(factors).toEqual([1, 2, 3, 4, 6, 12]); + }); +}); diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 34508e94a9f..6d73ab9fbd8 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,99 +1,5 @@ -import _ from 'lodash'; -import tinycolor from 'tinycolor2'; import config from 'app/core/config'; -export const PALETTE_ROWS = 4; -export const PALETTE_COLUMNS = 14; -export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; -export const OK_COLOR = 'rgba(11, 237, 50, 1)'; -export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; -export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; -export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; -export const REGION_FILL_ALPHA = 0.09; - -const colors = [ - '#7EB26D', // 0: pale green - '#EAB839', // 1: mustard - '#6ED0E0', // 2: light blue - '#EF843C', // 3: orange - '#E24D42', // 4: red - '#1F78C1', // 5: ocean - '#BA43A9', // 6: purple - '#705DA0', // 7: violet - '#508642', // 8: dark green - '#CCA300', // 9: dark sand - '#447EBC', - '#C15C17', - '#890F02', - '#0A437C', - '#6D1F62', - '#584477', - '#B7DBAB', - '#F4D598', - '#70DBED', - '#F9BA8F', - '#F29191', - '#82B5D8', - '#E5A8E2', - '#AEA2E0', - '#629E51', - '#E5AC0E', - '#64B0C8', - '#E0752D', - '#BF1B00', - '#0A50A1', - '#962D82', - '#614D93', - '#9AC48A', - '#F2C96D', - '#65C5DB', - '#F9934E', - '#EA6460', - '#5195CE', - '#D683CE', - '#806EB7', - '#3F6833', - '#967302', - '#2F575E', - '#99440A', - '#58140C', - '#052B51', - '#511749', - '#3F2B5B', - '#E0F9D7', - '#FCEACA', - '#CFFAFF', - '#F9E2D2', - '#FCE2DE', - '#BADFF4', - '#F9D9F9', - '#DEDAF7', -]; - -export function sortColorsByHue(hexColors) { - const hslColors = _.map(hexColors, hexToHsl); - - let sortedHSLColors = _.sortBy(hslColors, ['h']); - sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); - sortedHSLColors = _.map(sortedHSLColors, chunk => { - return _.sortBy(chunk, 'l'); - }); - sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); - - return _.map(sortedHSLColors, hslToHex); -} - -export function hexToHsl(color) { - return tinycolor(color).toHsl(); -} - -export function hslToHex(color) { - return tinycolor(color).toHexString(); -} - export function getThemeColor(dark: string, light: string): string { return config.bootData.user.lightTheme ? light : dark; } - -export let sortedColors = sortColorsByHue(colors); -export default colors; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index bea166075dc..f3273ffa16d 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,9 +1,9 @@ import _ from 'lodash'; +import { colors } from '@grafana/ui'; import { renderUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; import store from 'app/core/store'; -import colors from 'app/core/utils/colors'; import { parse as parseDate } from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; diff --git a/public/app/core/utils/factors.ts b/public/app/core/utils/factors.ts new file mode 100644 index 00000000000..e9ce327a631 --- /dev/null +++ b/public/app/core/utils/factors.ts @@ -0,0 +1,5 @@ +// Returns the factors of a number +// Example getFactors(12) -> [1, 2, 3, 4, 6, 12] +export default function getFactors(num: number): number[] { + return Array.from(new Array(num + 1), (_, i) => i).filter(i => num % i === 0); +} diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx index a5afbc198fc..2a1b3d12ecf 100644 --- a/public/app/features/alerting/AlertTab.tsx +++ b/public/app/features/alerting/AlertTab.tsx @@ -14,6 +14,7 @@ import 'app/features/alerting/AlertTabCtrl'; // Types import { DashboardModel } from '../dashboard/dashboard_model'; import { PanelModel } from '../dashboard/panel_model'; +import { TestRuleResult } from './TestRuleResult'; interface Props { angularPanel?: AngularComponent; @@ -65,9 +66,7 @@ export class AlertTab extends PureComponent { const loader = getAngularLoader(); const template = ''; - const scopeProps = { - ctrl: this.panelCtrl, - }; + const scopeProps = { ctrl: this.panelCtrl }; this.component = loader.load(this.element, scopeProps, template); } @@ -111,6 +110,16 @@ export class AlertTab extends PureComponent { }; }; + renderTestRuleResult = () => { + const { panel, dashboard } = this.props; + return ; + }; + + testRule = (): EditorToolbarView => ({ + title: 'Test Rule', + render: () => this.renderTestRuleResult(), + }); + onAddAlert = () => { this.panelCtrl._enableAlert(); this.component.digest(); @@ -120,7 +129,7 @@ export class AlertTab extends PureComponent { render() { const { alert } = this.props.panel; - const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : []; + const toolbarItems = alert ? [this.stateHistory(), this.testRule(), this.deleteAlert()] : []; const model = { title: 'Panel has no alert rule defined', diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 2be25e9df6a..af00e79b085 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -9,8 +9,6 @@ import appEvents from 'app/core/app_events'; export class AlertTabCtrl { panel: any; panelCtrl: any; - testing: boolean; - testResult: any; subTabIndex: number; conditionTypes: any; alert: any; @@ -406,21 +404,6 @@ export class AlertTabCtrl { }, }); } - - test() { - this.testing = true; - this.testResult = false; - - const payload = { - dashboard: this.dashboardSrv.getCurrent().getSaveModelClone(), - panelId: this.panelCtrl.panel.id, - }; - - return this.backendSrv.post('/api/alerts/test', payload).then(res => { - this.testResult = res; - this.testing = false; - }); - } } /** @ngInject */ diff --git a/public/app/features/alerting/TestRuleResult.test.tsx b/public/app/features/alerting/TestRuleResult.test.tsx new file mode 100644 index 00000000000..9beb5ade632 --- /dev/null +++ b/public/app/features/alerting/TestRuleResult.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import { Props, TestRuleResult } from './TestRuleResult'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => ({ + post: jest.fn(), + }), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + panelId: 1, + dashboard: new DashboardModel({ panels: [{ id: 1 }] }), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + + return { wrapper, instance: wrapper.instance() as TestRuleResult }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + describe('component did mount', () => { + it('should call testRule', () => { + const { instance } = setup(); + instance.testRule = jest.fn(); + instance.componentDidMount(); + + expect(instance.testRule).toHaveBeenCalled(); + }); + }); +}); diff --git a/public/app/features/alerting/TestRuleResult.tsx b/public/app/features/alerting/TestRuleResult.tsx new file mode 100644 index 00000000000..4014e529597 --- /dev/null +++ b/public/app/features/alerting/TestRuleResult.tsx @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react'; +import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import { LoadingPlaceholder } from '@grafana/ui/src'; + +export interface Props { + panelId: number; + dashboard: DashboardModel; +} + +interface State { + isLoading: boolean; + testRuleResponse: {}; +} + +export class TestRuleResult extends PureComponent { + readonly state: State = { + isLoading: false, + testRuleResponse: {}, + }; + + componentDidMount() { + this.testRule(); + } + + async testRule() { + const { panelId, dashboard } = this.props; + const payload = { dashboard: dashboard.getSaveModelClone(), panelId }; + + this.setState({ isLoading: true }); + const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload); + this.setState({ isLoading: false, testRuleResponse }); + } + + render() { + const { testRuleResponse, isLoading } = this.state; + + if (isLoading === true) { + return ; + } + + return ; + } +} diff --git a/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap b/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap new file mode 100644 index 00000000000..73f85f12354 --- /dev/null +++ b/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + +`; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index da862203da6..90e0c7bbac2 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -121,20 +121,6 @@
- -
- -
-
- -
- Evaluating rule -
- -
-
diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index db748e639a1..6966d3cdc82 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -1,8 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; import tinycolor from 'tinycolor2'; -import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { AnnotationEvent } from './event'; import { OK_COLOR, ALERTING_COLOR, @@ -10,7 +8,10 @@ import { PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, -} from 'app/core/utils/colors'; +} from '@grafana/ui'; + +import { MetricsPanelCtrl } from 'app/plugins/sdk'; +import { AnnotationEvent } from './event'; export class EventManager { event: AnnotationEvent; diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index abd12ab4b13..2dbeb6c6e80 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -9,6 +9,7 @@ import { } from 'app/core/constants'; import { PanelModel } from './panel_model'; import { DashboardModel } from './dashboard_model'; +import getFactors from 'app/core/utils/factors'; export class DashboardMigrator { dashboard: DashboardModel; @@ -21,7 +22,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades = []; - this.dashboard.schemaVersion = 16; + this.dashboard.schemaVersion = 17; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -368,6 +369,24 @@ export class DashboardMigrator { this.upgradeToGridLayout(old); } + if (oldVersion < 17) { + panelUpgrades.push(panel => { + if (panel.minSpan) { + const max = GRID_COLUMN_COUNT / panel.minSpan; + const factors = getFactors(GRID_COLUMN_COUNT); + // find the best match compared to factors + // (ie. [1,2,3,4,6,12,24] for 24 columns) + panel.maxPerRow = + factors[ + _.findIndex(factors, o => { + return o > max; + }) - 1 + ]; + } + delete panel.minSpan; + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 6f98bc5a17a..2ae2df0124b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,8 +1,8 @@ import moment from 'moment'; import _ from 'lodash'; +import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; -import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors'; import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; @@ -442,7 +442,7 @@ export class DashboardModel { } const selectedOptions = this.getSelectedVariableOptions(variable); - const minWidth = panel.minSpan || 6; + const maxPerRow = panel.maxPerRow || 4; let xPos = 0; let yPos = panel.gridPos.y; @@ -462,7 +462,7 @@ export class DashboardModel { } else { // set width based on how many are selected // assumed the repeated panels should take up full row width - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow); copy.gridPos.x = xPos; copy.gridPos.y = yPos; diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx index b159cb30a4b..e86baf0a80b 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -52,7 +52,7 @@ export class EditorTabBody extends PureComponent { onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, - isOpen: !this.state.isOpen, + isOpen: this.state.openView !== item || !this.state.isOpen, }); }; diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 77ab64b1dba..a20f8627fba 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,10 +1,10 @@ // Libraries -import React, { PureComponent, SFC } from 'react'; +import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components import 'app/features/panel/metrics_tab'; -import { EditorTabBody, EditorToolbarView} from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; @@ -36,12 +36,6 @@ interface State { isAddingMixed: boolean; } -interface LoadingPlaceholderProps { - text: string; -} - -const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; - export class QueriesTab extends PureComponent { element: HTMLElement; component: AngularComponent; @@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent { renderQueryInspector = () => { const { panel } = this.props; - return ; + return ; }; renderHelp = () => { diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 090bc220bc0..8e490f6b622 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import appEvents from 'app/core/app_events'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; +import { LoadingPlaceholder } from '@grafana/ui'; interface DsQuery { isLoading: boolean; @@ -10,7 +11,6 @@ interface DsQuery { interface Props { panel: any; - LoadingPlaceholder: any; } interface State { @@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent { render() { const { response, isLoading } = this.state.dsQuery; - const { LoadingPlaceholder } = this.props; const { isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2d5a70b47dd..2fec8e379dd 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -77,7 +77,7 @@ export class PanelModel { repeatPanelId?: number; repeatDirection?: string; repeatedByRow?: boolean; - minSpan?: number; + maxPerRow?: number; collapsed?: boolean; panels?: any; soloMode?: boolean; diff --git a/public/app/features/dashboard/specs/dashboard_migration.test.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts index 5f693c9f6d9..e15bd65d5a5 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.test.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.test.ts @@ -127,7 +127,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(16); + expect(model.schemaVersion).toBe(17); }); it('graph thresholds should be migrated', () => { @@ -364,14 +364,6 @@ describe('DashboardModel', () => { expect(dashboard.panels.length).toBe(2); }); - it('minSpan should be twice', () => { - model.rows = [createRow({ height: 8 }, [[6]])]; - model.rows[0].panels[0] = { minSpan: 12 }; - - const dashboard = new DashboardModel(model); - expect(dashboard.panels[0].minSpan).toBe(24); - }); - it('should assign id', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows[0].panels[0] = {}; @@ -380,6 +372,16 @@ describe('DashboardModel', () => { expect(dashboard.panels[0].id).toBe(1); }); }); + + describe('when migrating from minSpan to maxPerRow', () => { + it('maxPerRow should be correct', () => { + const model = { + panels: [{ minSpan: 8 }], + }; + const dashboard = new DashboardModel(model); + expect(dashboard.panels[0].maxPerRow).toBe(3); + }); + }); }); function createRow(options, panelDescriptions: any[]) { diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index f7ed0efd910..cf00a31c71e 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -143,12 +143,9 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange) const timeShift = '-' + timeShiftInterpolated; newTimeData.timeInfo += ' timeshift ' + timeShift; newTimeData.timeRange = { - from: dateMath.parseDateMath(timeShift, timeRange.from, false), - to: dateMath.parseDateMath(timeShift, timeRange.to, true), - raw: { - from: timeRange.from, - to: timeRange.to, - }, + from: dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false), + to: dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true), + raw: newTimeData.timeRange.raw, }; } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 432d22fecdf..f68423315d7 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -5,6 +5,7 @@ import Remarkable from 'remarkable'; import config from 'app/core/config'; import { profiler } from 'app/core/core'; import { Emitter } from 'app/core/core'; +import getFactors from 'app/core/utils/factors'; import { duplicatePanel, copyPanel as copyPanelUtil, @@ -12,7 +13,7 @@ import { sharePanel as sharePanelUtil, } from 'app/features/dashboard/utils/panel'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants'; export class PanelCtrl { panel: any; @@ -32,6 +33,7 @@ export class PanelCtrl { events: Emitter; timing: any; loading: boolean; + maxPanelsPerRowOptions: number[]; constructor($scope, $injector) { this.$injector = $injector; @@ -92,6 +94,7 @@ export class PanelCtrl { if (!this.editModeInitiated) { this.editModeInitiated = true; this.events.emit('init-edit-mode', null); + this.maxPanelsPerRowOptions = getFactors(GRID_COLUMN_COUNT); } } diff --git a/public/app/features/panel/partials/general_tab.html b/public/app/features/panel/partials/general_tab.html index d6c2d4804a0..8881d2c28a4 100644 --- a/public/app/features/panel/partials/general_tab.html +++ b/public/app/features/panel/partials/general_tab.html @@ -32,12 +32,17 @@
- Min width -
+
+
+ Note: You may need to change the variable selection to see this in action. +
+
diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index c2f2364d49d..3781a9048a6 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -254,6 +254,10 @@ export class ElasticDatasource { continue; } + if (target.alias) { + target.alias = this.templateSrv.replace(target.alias, options.scopedVars, 'lucene'); + } + const queryString = this.templateSrv.replace(target.query || '*', options.scopedVars, 'lucene'); const queryObj = this.queryBuilder.build(target, adhocFilters, queryString); const esQuery = angular.toJson(queryObj); diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 4be0c35852c..0480fcb52e8 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -16,7 +16,13 @@ describe('ElasticDatasource', function(this: any) { }; const templateSrv = { - replace: jest.fn(text => text), + replace: jest.fn(text => { + if (text.startsWith("$")) { + return `resolvedVariable`; + } else { + return text; + } + }), getAdhocFilters: jest.fn(() => []), }; @@ -67,7 +73,7 @@ describe('ElasticDatasource', function(this: any) { }); describe('When issuing metric query with interval pattern', () => { - let requestOptions, parts, header; + let requestOptions, parts, header, query; beforeEach(() => { createDatasource({ @@ -81,19 +87,22 @@ describe('ElasticDatasource', function(this: any) { return Promise.resolve({ data: { responses: [] } }); }); - ctx.ds.query({ + query = { range: { from: moment.utc([2015, 4, 30, 10]), to: moment.utc([2015, 5, 1, 10]), }, targets: [ { + alias: "$varAlias", bucketAggs: [], metrics: [{ type: 'raw_document' }], query: 'escape\\:test', }, ], - }); + }; + + ctx.ds.query(query); parts = requestOptions.data.split('\n'); header = angular.fromJson(parts[0]); @@ -103,6 +112,10 @@ describe('ElasticDatasource', function(this: any) { expect(header.index).toEqual(['asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']); }); + it('should resolve the alias variable', () => { + expect(query.targets[0].alias).toEqual('resolvedVariable'); + }); + it('should json escape lucene query', () => { const body = angular.fromJson(parts[1]); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); diff --git a/public/app/plugins/datasource/influxdb/query_builder.ts b/public/app/plugins/datasource/influxdb/query_builder.ts index a61216787d3..0b3e6f01e74 100644 --- a/public/app/plugins/datasource/influxdb/query_builder.ts +++ b/public/app/plugins/datasource/influxdb/query_builder.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; function renderTagCondition(tag, index) { let str = ''; @@ -43,7 +44,7 @@ export class InfluxQueryBuilder { } else if (type === 'MEASUREMENTS') { query = 'SHOW MEASUREMENTS'; if (withMeasurementFilter) { - query += ' WITH MEASUREMENT =~ /' + withMeasurementFilter + '/'; + query += ' WITH MEASUREMENT =~ /' + kbn.regexEscape(withMeasurementFilter) + '/'; } } else if (type === 'FIELDS') { measurement = this.target.measurement; diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts index e21b95ac374..ee617e7e774 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts @@ -50,6 +50,12 @@ describe('InfluxQueryBuilder', () => { expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100'); }); + it('should escape the regex value in measurement query', () => { + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'abc/edf/'); + expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /abc\\/edf\\// LIMIT 100'); + }); + it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', diff --git a/public/app/plugins/panel/gauge/GaugeOptions.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx similarity index 91% rename from public/app/plugins/panel/gauge/GaugeOptions.tsx rename to public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index 7607374b1b7..cb436180b49 100644 --- a/public/app/plugins/panel/gauge/GaugeOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -1,10 +1,10 @@ import React, { PureComponent } from 'react'; +import { GaugeOptions, PanelOptionsProps } from '@grafana/ui'; + import { Switch } from 'app/core/components/Switch/Switch'; import { Label } from '../../../core/components/Label/Label'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; -export default class GaugeOptions extends PureComponent> { +export default class GaugeOptionsEditor 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 5f1a438863f..79220daf37a 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,10 +1,10 @@ import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode } from '@grafana/ui'; +import { GaugeOptions, PanelProps, NullValueMode } from '@grafana/ui'; + import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; import Gauge from 'app/viz/Gauge'; -import { Options } from './types'; -interface Props extends PanelProps {} +interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index 2b16ef5a1fe..e43abad61a3 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -1,11 +1,9 @@ import React, { PureComponent } from 'react'; +import { BasicGaugeColor, GaugeOptions, PanelOptionsProps, ThresholdsEditor, Threshold } from '@grafana/ui'; + import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import Thresholds from 'app/plugins/panel/gauge/Thresholds'; -import { BasicGaugeColor } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; import ValueMappings from 'app/plugins/panel/gauge/ValueMappings'; -import { Options } from './types'; -import GaugeOptions from './GaugeOptions'; +import GaugeOptionsEditor from './GaugeOptionsEditor'; export const defaultProps = { options: { @@ -24,17 +22,19 @@ export const defaultProps = { }, }; -export default class GaugePanelOptions extends PureComponent> { +export default class GaugePanelOptions extends PureComponent> { static defaultProps = defaultProps; + onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds }); + render() { const { onChange, options } = this.props; return ( <>
- - + +
diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index 35d0b2e638c..b975821f27a 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; +import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; + import { Label } from 'app/core/components/Label/Label'; -import { Select } from 'app/core/components/Select/Select'; -import { MappingType, RangeMap, ValueMap } from 'app/types'; interface Props { mapping: ValueMap | RangeMap; diff --git a/public/app/plugins/panel/gauge/ValueMappings.test.tsx b/public/app/plugins/panel/gauge/ValueMappings.test.tsx index 3e59cc76742..07db4028c68 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.test.tsx +++ b/public/app/plugins/panel/gauge/ValueMappings.test.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ValueMappings from './ValueMappings'; -import { MappingType } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; +import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui'; import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions'; +import ValueMappings from './ValueMappings'; + const setup = (propOverrides?: object) => { - const props: PanelOptionsProps = { + const props: PanelOptionsProps = { onChange: jest.fn(), options: { ...defaultProps.options, diff --git a/public/app/plugins/panel/gauge/ValueMappings.tsx b/public/app/plugins/panel/gauge/ValueMappings.tsx index be800cf2412..4ce0d37b53c 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.tsx +++ b/public/app/plugins/panel/gauge/ValueMappings.tsx @@ -1,15 +1,14 @@ import React, { PureComponent } from 'react'; +import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap } from '@grafana/ui'; + import MappingRow from './MappingRow'; -import { MappingType, RangeMap, ValueMap } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; interface State { mappings: Array; nextIdToAdd: number; } -export default class ValueMappings extends PureComponent, State> { +export default class ValueMappings extends PureComponent, State> { constructor(props) { super(props); diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/ValueOptions.tsx index 4aafc0b0457..0b30da35d36 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/ValueOptions.tsx @@ -1,9 +1,9 @@ import React, { PureComponent } from 'react'; +import { GaugeOptions, PanelOptionsProps } from '@grafana/ui'; + import { Label } from 'app/core/components/Label/Label'; -import Select from 'app/core/components/Select/Select'; +import { Select} from '@grafana/ui'; import UnitPicker from 'app/core/components/Select/UnitPicker'; -import { PanelOptionsProps } from '@grafana/ui'; -import { Options } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -21,7 +21,7 @@ const statOptions = [ const labelWidth = 6; -export default class ValueOptions extends PureComponent> { +export default class ValueOptions extends PureComponent> { onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index 2105687d8e1..d6df17d9699 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { TimeSeries } from 'app/core/core'; -import { SeriesColorPicker } from 'app/core/components/colorpicker/SeriesColorPicker'; +import { SeriesColorPicker } from '@grafana/ui'; export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 4ea1efe1502..4fe47b70129 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; +import { colors } from '@grafana/ui'; + import TimeSeries from 'app/core/time_series2'; -import colors from 'app/core/utils/colors'; export class DataProcessor { constructor(private panel) {} diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 6a1b5806aaa..28c17dbad2c 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -1,7 +1,7 @@ // Libraries import _ from 'lodash'; import React, { PureComponent } from 'react'; -import colors from 'app/core/utils/colors'; +import { colors } from '@grafana/ui'; // Utils import { processTimeSeries } from '@grafana/ui/src/utils'; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 75a34ac01c0..4e4dd8121cf 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -1,12 +1,12 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; +import Drop from 'tether-drop'; +import { colors } from '@grafana/ui'; import coreModule from 'app/core/core_module'; import { profiler } from 'app/core/profiler'; import appEvents from 'app/core/app_events'; -import Drop from 'tether-drop'; -import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv'; import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index ab52b03ab17..72da1c76ea8 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,6 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState, UserState } from './user'; import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources'; import { DataQuery, DataQueryResponse, DataQueryOptions } from './series'; -import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins'; import { Organization, OrganizationState } from './organization'; import { @@ -69,13 +68,8 @@ export { AppNotificationTimeout, DashboardSearchHit, UserState, - Threshold, ValidationEvents, ValidationRule, - ValueMap, - RangeMap, - MappingType, - BasicGaugeColor, }; export interface StoreState { diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts deleted file mode 100644 index 31674d20304..00000000000 --- a/public/app/types/panel.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface Threshold { - index: number; - value: number; - color?: string; -} - -export enum MappingType { - ValueToText = 1, - RangeToText = 2, -} - -export enum BasicGaugeColor { - Green = '#299c46', - Red = '#d44a3a', -} - -interface BaseMap { - id: number; - operator: string; - text: string; - type: MappingType; -} - -export interface ValueMap extends BaseMap { - value: string; -} - -export interface RangeMap extends BaseMap { - from: string; - to: string; -} diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index 91107a563e5..f0c4a874649 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui'; + import { Gauge, Props } from './Gauge'; -import { BasicGaugeColor } from '../types'; -import { TimeSeriesVMs } from '@grafana/ui'; jest.mock('jquery', () => ({ plot: jest.fn(), diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index defeaf8cc8f..5112ff9aa1b 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from 'app/types'; -import { TimeSeriesVMs } from '@grafana/ui'; +import { BasicGaugeColor, Threshold, TimeSeriesVMs, RangeMap, ValueMap, MappingType } from '@grafana/ui'; + import config from '../core/config'; import kbn from '../core/utils/kbn'; diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts index 782383957bc..5f27974a33b 100644 --- a/public/app/viz/state/timeSeries.ts +++ b/public/app/viz/state/timeSeries.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; // Utils -import colors from 'app/core/utils/colors'; +import { colors } from '@grafana/ui'; // Types import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui'; diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 55cf7242aa6..f2c441053bb 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -65,7 +65,7 @@ } ], "rows": [], - "schemaVersion": 16, + "schemaVersion": 17, "style": "dark", "tags": [], "templating": { diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 10cc7335bdf..b8498f18b19 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,4 +1,4 @@ -// DEPENDENCIES + // DEPENDENCIES @import '../../node_modules/react-table/react-table.css'; // VENDOR @@ -38,9 +38,6 @@ @import 'layout/lists'; @import 'layout/page'; -// LOAD @grafana/ui components -@import '../../packages/grafana-ui/src/index'; - // COMPONENTS @import 'components/scrollbar'; @import 'components/cards'; @@ -97,16 +94,17 @@ @import 'components/page_header'; @import 'components/dashboard_settings'; @import 'components/empty_list_cta'; -@import 'components/form_select_box'; @import 'components/panel_editor'; @import 'components/toolbar'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; -@import 'components/thresholds'; @import 'components/toggle_button_group'; @import 'components/value-mappings'; @import 'components/popover-box'; +// LOAD @grafana/ui components +@import '../../packages/grafana-ui/src/index'; + // PAGES @import 'pages/login'; @import 'pages/dashboard'; diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 3013452a279..980ef5cc4c2 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -59,7 +59,7 @@ go run build.go ${OPT} build-frontend source /etc/profile.d/rvm.sh echo "Packaging" -go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest +go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only #removing amd64 phantomjs bin for armv7/arm64 packages rm tools/phantomjs/phantomjs go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only @@ -80,3 +80,4 @@ else fi go run build.go -goos windows -pkg-arch amd64 ${OPT} package-only +go run build.go latest \ No newline at end of file diff --git a/scripts/build/build.sh b/scripts/build/build.sh index 8362942c6cd..ac6aab0b867 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -8,6 +8,8 @@ set -e EXTRA_OPTS="$@" +CCARMV7=arm-linux-gnueabihf-gcc +CCARM64=aarch64-linux-gnu-gcc CCX64=/tmp/x86_64-centos6-linux-gnu/bin/x86_64-centos6-linux-gnu-gcc GOPATH=/go @@ -26,6 +28,9 @@ fi echo "Build arguments: $OPT" +go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build +go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build + CC=${CCX64} go run build.go ${OPT} build yarn install --pure-lockfile --no-progress @@ -43,4 +48,8 @@ go run build.go ${OPT} build-frontend source /etc/profile.d/rvm.sh echo "Packaging" -go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest +go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only +go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only +go run build.go -goos linux -pkg-arch arm64 ${OPT} package-only + +go run build.go latest diff --git a/scripts/webpack/IgnoreNotFoundExportPlugin.js b/scripts/webpack/IgnoreNotFoundExportPlugin.js new file mode 100644 index 00000000000..37bf1f7200e --- /dev/null +++ b/scripts/webpack/IgnoreNotFoundExportPlugin.js @@ -0,0 +1,22 @@ +// https://github.com/TypeStrong/ts-loader/issues/653#issuecomment-390889335 + +const ModuleDependencyWarning = require("webpack/lib/ModuleDependencyWarning") + +module.exports = class IgnoreNotFoundExportPlugin { + apply(compiler) { + const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/ + function doneHook(stats) { + stats.compilation.warnings = stats.compilation.warnings.filter(function(warn) { + if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) { + return false + } + return true; + }) + } + if (compiler.hooks) { + compiler.hooks.done.tap("IgnoreNotFoundExportPlugin", doneHook) + } else { + compiler.plugin("done", doneHook) + } + } +} diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index ca08ff1e726..b37e4c08592 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -7,6 +7,7 @@ const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); +const IgnoreNotFoundExportPlugin = require("./IgnoreNotFoundExportPlugin.js"); module.exports = merge(common, { entry: { @@ -111,5 +112,6 @@ module.exports = merge(common, { NODE_ENV: JSON.stringify('development'), }, }), + new IgnoreNotFoundExportPlugin(), ], }); diff --git a/yarn.lock b/yarn.lock index 8eff64ca822..376a0b1d23a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1098,14 +1098,14 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^2.0.15": +"@types/react-transition-group@*", "@types/react-transition-group@^2.0.15": version "2.0.15" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.15.tgz#e5ee3fe558832e141cc6041bdd54caea7b787af8" integrity sha512-S0QnNzbHoWXDbKBl/xk5dxA4FT+BNlBcI3hku991cl8Cz3ytOkUMcCRtzdX11eb86E131bSsQqy5WrPCdJYblw== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.1.0", "@types/react@^16.7.6": +"@types/react@*", "@types/react@16.7.6", "@types/react@^16.1.0", "@types/react@^16.7.6": version "16.7.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040" integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA== @@ -1118,6 +1118,23 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-0.2.5.tgz#2443fc12da514c81346b1a665675559cee21fa75" integrity sha512-dEoVvo/I9QFomyhY+4Q6Qk+I+dhG59TYceZgC6Q0mCifVPErx6Y83PNTKGDS5e9h9Eti6q0S2mm16BU6iQK+3w== +"@types/tether-drop@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@types/tether-drop/-/tether-drop-1.4.8.tgz#8d64288e673259d1bc28518250b80b5ef43af0bc" + integrity sha512-QzrJDUxnLoqACUm7opxGOwa9mgMBlkyb7hHYWApMLM3ywWif4pWraTiotooiG3ePZmnTe8wQj2nx7GWMX4pb+w== + dependencies: + "@types/tether" "*" + +"@types/tether@*": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@types/tether/-/tether-1.4.4.tgz#0fde1ccbd2f1fad74f8f465fe6227ff3b7bff634" + integrity sha512-6qhsFJVMuMqaQRVyQVi3zUBLfKYyryktL0ZP0Z3zegzeQ7WKm0PZNCdl3JsaitJbzqaoQ9qsFKMfaj5MiMfcSQ== + +"@types/tinycolor2@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.1.tgz#2f5670c9d1d6e558897a810ed284b44918fc1253" + integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg== + "@types/uglify-js@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.3.tgz#801a5ca1dc642861f47c46d14b700ed2d610840b" @@ -3168,7 +3185,7 @@ caniuse-api@^1.5.2: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: +caniuse-db@1.0.30000772, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: version "1.0.30000772" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" integrity sha1-UarokXaChureSj2DGep21qAbUSs=