Просмотр исходного кода

Merge branch 'master' into grafana-ui/select

Dominik Prokop 7 лет назад
Родитель
Сommit
391253ee2d
60 измененных файлов с 477 добавлено и 304 удалено
  1. 25 25
      .circleci/config.yml
  2. 1 0
      CHANGELOG.md
  3. 2 0
      build.go
  4. 1 1
      docs/sources/reference/dashboard.md
  5. 4 2
      docs/sources/reference/templating.md
  6. 6 1
      packages/grafana-ui/package.json
  7. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx
  8. 3 3
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx
  9. 4 9
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  10. 12 21
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  11. 2 1
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx
  12. 6 10
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
  13. 4 4
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  14. 0 0
      packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap
  15. 11 0
      packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx
  16. 5 0
      packages/grafana-ui/src/components/index.ts
  17. 93 0
      packages/grafana-ui/src/utils/colors.ts
  18. 1 0
      packages/grafana-ui/src/utils/index.ts
  19. 4 2
      packaging/docker/Dockerfile
  20. 0 1
      packaging/docker/build-deploy.sh
  21. 32 8
      packaging/docker/build.sh
  22. 29 7
      packaging/docker/push_to_docker_hub.sh
  23. 1 1
      pkg/models/dashboards.go
  24. 1 1
      pkg/services/alerting/extractor.go
  25. 18 3
      pkg/services/alerting/rule.go
  26. 60 20
      pkg/services/alerting/rule_test.go
  27. 10 0
      public/app/core/angular_wrappers.ts
  28. 1 3
      public/app/core/core.ts
  29. 3 1
      public/app/core/logs_model.ts
  30. 8 0
      public/app/core/specs/factors.test.ts
  31. 0 94
      public/app/core/utils/colors.ts
  32. 1 1
      public/app/core/utils/explore.ts
  33. 5 0
      public/app/core/utils/factors.ts
  34. 5 15
      public/app/features/alerting/AlertTab.tsx
  35. 3 4
      public/app/features/alerting/TestRuleResult.test.tsx
  36. 5 4
      public/app/features/alerting/TestRuleResult.tsx
  37. 0 13
      public/app/features/alerting/__snapshots__/TestRuleButton.test.tsx.snap
  38. 7 0
      public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap
  39. 4 3
      public/app/features/annotations/event_manager.ts
  40. 20 1
      public/app/features/dashboard/dashboard_migration.ts
  41. 3 3
      public/app/features/dashboard/dashboard_model.ts
  42. 3 9
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  43. 1 2
      public/app/features/dashboard/dashgrid/QueryInspector.tsx
  44. 1 1
      public/app/features/dashboard/panel_model.ts
  45. 11 9
      public/app/features/dashboard/specs/dashboard_migration.test.ts
  46. 3 6
      public/app/features/dashboard/utils/panel.ts
  47. 4 1
      public/app/features/panel/panel_ctrl.ts
  48. 7 2
      public/app/features/panel/partials/general_tab.html
  49. 2 1
      public/app/plugins/datasource/influxdb/query_builder.ts
  50. 6 0
      public/app/plugins/datasource/influxdb/specs/query_builder.test.ts
  51. 1 1
      public/app/plugins/panel/gauge/Thresholds.tsx
  52. 1 1
      public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx
  53. 2 1
      public/app/plugins/panel/graph/data_processor.ts
  54. 1 1
      public/app/plugins/panel/graph2/GraphPanel.tsx
  55. 2 2
      public/app/routes/GrafanaCtrl.ts
  56. 1 1
      public/app/viz/state/timeSeries.ts
  57. 1 1
      public/dashboards/home.json
  58. 2 1
      scripts/build/build-all.sh
  59. 10 1
      scripts/build/build.sh
  60. 17 0
      yarn.lock

+ 25 - 25
.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

+ 1 - 0
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)

+ 2 - 0
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 {

+ 1 - 1
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": []
 }

+ 4 - 2
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.

+ 6 - 1
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"
   }

+ 1 - 1
public/app/core/specs/ColorPalette.test.tsx → 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', () => {

+ 3 - 3
public/app/core/components/colorpicker/ColorPalette.tsx → 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<Props, any> {
   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);
     };

+ 4 - 9
public/app/core/components/colorpicker/ColorPicker.tsx → 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<Props, any> {
-  pickerElem: HTMLElement;
+  pickerElem: HTMLElement | null;
   colorPickerDrop: any;
 
   openColorPicker = () => {
@@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
     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<Props, any> {
       hoverCloseDelay: 200,
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+        attachment: 'bottom center',
       },
     });
 
@@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
     }, 100);
   };
 
-  onColorSelect = color => {
+  onColorSelect = (color: string) => {
     this.props.onChange(color);
   };
 
@@ -59,8 +59,3 @@ export class ColorPicker extends React.Component<Props, any> {
     );
   }
 }
-
-react2AngularDirective('colorPicker', ColorPicker, [
-  'color',
-  ['onChange', { watchDepth: 'reference', wrapApply: true }],
-]);

+ 12 - 21
public/app/core/components/colorpicker/ColorPickerPopover.tsx → packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -14,7 +14,7 @@ export interface Props {
 export class ColorPickerPopover extends React.Component<Props, any> {
   pickerNavElem: any;
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     this.state = {
       tab: 'palette',
@@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
     };
   }
 
-  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 });
     });
   }
 

+ 2 - 1
public/app/core/components/colorpicker/SeriesColorPicker.tsx → packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx

@@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
     onToggleAxis: () => {},
   };
 
-  constructor(props) {
+  constructor(props: SeriesColorPickerProps) {
     super(props);
   }
 
@@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
       remove: true,
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+        attachment: 'bottom center',
       },
     });
 

+ 6 - 10
public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx → 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<SeriesColorPic
 
 interface AxisSelectorProps {
   yaxis: number;
-  onToggleAxis: () => void;
+  onToggleAxis?: () => void;
 }
 
 interface AxisSelectorState {
@@ -30,7 +29,7 @@ interface AxisSelectorState {
 }
 
 export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
-  constructor(props) {
+  constructor(props: AxisSelectorProps) {
     super(props);
     this.state = {
       yaxis: this.props.yaxis,
@@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
     this.setState({
       yaxis: this.state.yaxis === 2 ? 1 : 2,
     });
-    this.props.onToggleAxis();
+
+    if (this.props.onToggleAxis) {
+      this.props.onToggleAxis();
+    }
   }
 
   render() {
@@ -62,9 +64,3 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
     );
   }
 }
-
-react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
-  'series',
-  'onColorChange',
-  'onToggleAxis',
-]);

+ 4 - 4
public/app/core/components/colorpicker/SpectrumPicker.tsx → packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx

@@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
   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<Props, any> {
     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

+ 0 - 0
public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap → packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap


+ 11 - 0
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<LoadingPlaceholderProps> = ({ text }) => (
+  <div className="gf-form-group">
+    {text} <i className="fa fa-spinner fa-spin" />
+  </div>
+);

+ 5 - 0
packages/grafana-ui/src/components/index.ts

@@ -8,3 +8,8 @@ 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';

+ 93 - 0
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);

+ 1 - 0
packages/grafana-ui/src/utils/index.ts

@@ -1 +1,2 @@
 export * from './processTimeSeries';
+export * from './colors';

+ 4 - 2
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"

+ 0 - 1
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

+ 32 - 8
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

+ 29 - 7
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

+ 1 - 1
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

+ 1 - 1
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()

+ 18 - 3
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()

+ 60 - 20
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)
+		})
 	})
 }

+ 10 - 0
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',
+  ]);
 }

+ 1 - 3
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';

+ 3 - 1
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.

+ 8 - 0
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]);
+  });
+});

+ 0 - 94
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;

+ 1 - 1
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';

+ 5 - 0
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);
+}

+ 5 - 15
public/app/features/alerting/AlertTab.tsx

@@ -1,5 +1,5 @@
 // Libraries
-import React, { PureComponent, SFC } from 'react';
+import React, { PureComponent } from 'react';
 
 // Services & Utils
 import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
@@ -14,7 +14,7 @@ import 'app/features/alerting/AlertTabCtrl';
 // Types
 import { DashboardModel } from '../dashboard/dashboard_model';
 import { PanelModel } from '../dashboard/panel_model';
-import { TestRuleButton } from './TestRuleButton';
+import { TestRuleResult } from './TestRuleResult';
 
 interface Props {
   angularPanel?: AngularComponent;
@@ -22,16 +22,6 @@ interface Props {
   panel: PanelModel;
 }
 
-interface LoadingPlaceholderProps {
-  text: string;
-}
-
-const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
-  <div className="gf-form-group">
-    {text} <i className="fa fa-spinner fa-spin" />
-  </div>
-);
-
 export class AlertTab extends PureComponent<Props> {
   element: any;
   component: AngularComponent;
@@ -120,14 +110,14 @@ export class AlertTab extends PureComponent<Props> {
     };
   };
 
-  renderTestRuleButton = () => {
+  renderTestRuleResult = () => {
     const { panel, dashboard } = this.props;
-    return <TestRuleButton panelId={panel.id} dashboard={dashboard} LoadingPlaceholder={LoadingPlaceholder} />;
+    return <TestRuleResult panelId={panel.id} dashboard={dashboard} />;
   };
 
   testRule = (): EditorToolbarView => ({
     title: 'Test Rule',
-    render: () => this.renderTestRuleButton(),
+    render: () => this.renderTestRuleResult(),
   });
 
   onAddAlert = () => {

+ 3 - 4
public/app/features/alerting/TestRuleButton.test.tsx → public/app/features/alerting/TestRuleResult.test.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import { DashboardModel } from '../dashboard/dashboard_model';
-import { Props, TestRuleButton } from './TestRuleButton';
+import { Props, TestRuleResult } from './TestRuleResult';
 
 jest.mock('app/core/services/backend_srv', () => ({
   getBackendSrv: () => ({
@@ -13,14 +13,13 @@ const setup = (propOverrides?: object) => {
   const props: Props = {
     panelId: 1,
     dashboard: new DashboardModel({ panels: [{ id: 1 }] }),
-    LoadingPlaceholder: {},
   };
 
   Object.assign(props, propOverrides);
 
-  const wrapper = shallow(<TestRuleButton {...props} />);
+  const wrapper = shallow(<TestRuleResult {...props} />);
 
-  return { wrapper, instance: wrapper.instance() as TestRuleButton };
+  return { wrapper, instance: wrapper.instance() as TestRuleResult };
 };
 
 describe('Render', () => {

+ 5 - 4
public/app/features/alerting/TestRuleButton.tsx → public/app/features/alerting/TestRuleResult.tsx

@@ -2,11 +2,11 @@ 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;
-  LoadingPlaceholder: any;
 }
 
 interface State {
@@ -14,7 +14,7 @@ interface State {
   testRuleResponse: {};
 }
 
-export class TestRuleButton extends PureComponent<Props, State> {
+export class TestRuleResult extends PureComponent<Props, State> {
   readonly state: State = {
     isLoading: false,
     testRuleResponse: {},
@@ -27,13 +27,14 @@ export class TestRuleButton extends PureComponent<Props, State> {
   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(prevState => ({ ...prevState, isLoading: false, testRuleResponse }));
+    this.setState({ isLoading: false, testRuleResponse });
   }
 
   render() {
     const { testRuleResponse, isLoading } = this.state;
-    const { LoadingPlaceholder } = this.props;
 
     if (isLoading === true) {
       return <LoadingPlaceholder text="Evaluating rule" />;

+ 0 - 13
public/app/features/alerting/__snapshots__/TestRuleButton.test.tsx.snap

@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-<JSONFormatter
-  config={
-    Object {
-      "animateOpen": true,
-    }
-  }
-  json={Object {}}
-  open={3}
-/>
-`;

+ 7 - 0
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`] = `
+<Component
+  text="Evaluating rule"
+/>
+`;

+ 4 - 3
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;

+ 20 - 1
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;
     }

+ 3 - 3
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;
 

+ 3 - 9
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<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
-
 export class QueriesTab extends PureComponent<Props, State> {
   element: HTMLElement;
   component: AngularComponent;
@@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> {
 
   renderQueryInspector = () => {
     const { panel } = this.props;
-    return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
+    return <QueryInspector panel={panel} />;
   };
 
   renderHelp = () => {

+ 1 - 2
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<Props, State> {
 
   render() {
     const { response, isLoading } = this.state.dsQuery;
-    const { LoadingPlaceholder } = this.props;
     const { isMocking } = this.state;
     const openNodes = this.getNrOfOpenNodes();
 

+ 1 - 1
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;

+ 11 - 9
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[]) {

+ 3 - 6
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,
     };
   }
 

+ 4 - 1
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);
     }
   }
 

+ 7 - 2
public/app/features/panel/partials/general_tab.html

@@ -32,12 +32,17 @@
         </select>
       </div>
       <div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
-        <span class="gf-form-label width-9">Min width</span>
-        <select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
+        <span class="gf-form-label width-9">Max per row</span>
+        <select class="gf-form-input" ng-model="ctrl.panel.maxPerRow" ng-options="f for f in [2,3,4,6,12,24]">
           <option value=""></option>
         </select>
       </div>
+      <div class="gf-form-hint">
+        <div class="gf-form-hint-text muted">
+          Note: You may need to change the variable selection to see this in action.
+        </div>
       </div>
+    </div>
   </div>
 </div>
 

+ 2 - 1
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;

+ 6 - 0
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: '',

+ 1 - 1
public/app/plugins/panel/gauge/Thresholds.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import tinycolor from 'tinycolor2';
-import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
+import { ColorPicker } from '@grafana/ui';
 import { BasicGaugeColor, Threshold } from 'app/types';
 import { PanelOptionsProps } from '@grafana/ui';
 import { Options } from './types';

+ 1 - 1
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'];
 

+ 2 - 1
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) {}

+ 1 - 1
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';
 
 // Components & Types
 import { Graph, PanelProps, NullValueMode, processTimeSeries } from '@grafana/ui';

+ 2 - 2
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';

+ 1 - 1
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';

+ 1 - 1
public/dashboards/home.json

@@ -65,7 +65,7 @@
     }
   ],
   "rows": [],
-  "schemaVersion": 16,
+  "schemaVersion": 17,
   "style": "dark",
   "tags": [],
   "templating": {

+ 2 - 1
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

+ 10 - 1
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

+ 17 - 0
yarn.lock

@@ -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"