瀏覽代碼

Merge branch 'master' into kbn-formats-refactor

Torkel Ödegaard 7 年之前
父節點
當前提交
ba9d5115d2
共有 100 個文件被更改,包括 783 次插入511 次删除
  1. 25 25
      .circleci/config.yml
  2. 1 0
      CHANGELOG.md
  3. 3 1
      README.md
  4. 2 0
      build.go
  5. 1 0
      devenv/docker/blocks/influxdb/influxdb.conf
  6. 1 1
      docs/sources/reference/dashboard.md
  7. 4 2
      docs/sources/reference/templating.md
  8. 6 1
      packages/grafana-ui/package.json
  9. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx
  10. 3 3
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx
  11. 4 9
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  12. 12 21
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  13. 2 1
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx
  14. 6 10
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
  15. 4 4
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  16. 0 0
      packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap
  17. 11 0
      packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx
  18. 4 1
      packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx
  19. 4 0
      packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx
  20. 13 8
      packages/grafana-ui/src/components/Select/Select.tsx
  21. 22 12
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  22. 6 3
      packages/grafana-ui/src/components/Select/SelectOption.tsx
  23. 2 2
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  24. 0 0
      packages/grafana-ui/src/components/Select/_Select.scss
  25. 7 2
      packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap
  26. 27 0
      packages/grafana-ui/src/components/Select/resetSelectStyles.ts
  27. 7 15
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
  28. 41 47
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  29. 0 0
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  30. 2 0
      packages/grafana-ui/src/components/index.scss
  31. 12 0
      packages/grafana-ui/src/components/index.ts
  32. 2 2
      packages/grafana-ui/src/types/gauge.ts
  33. 1 0
      packages/grafana-ui/src/types/index.ts
  34. 32 0
      packages/grafana-ui/src/types/panel.ts
  35. 93 0
      packages/grafana-ui/src/utils/colors.ts
  36. 1 0
      packages/grafana-ui/src/utils/index.ts
  37. 4 2
      packaging/docker/Dockerfile
  38. 0 1
      packaging/docker/build-deploy.sh
  39. 32 8
      packaging/docker/build.sh
  40. 29 7
      packaging/docker/push_to_docker_hub.sh
  41. 1 1
      pkg/models/dashboards.go
  42. 1 1
      pkg/services/alerting/extractor.go
  43. 18 3
      pkg/services/alerting/rule.go
  44. 60 20
      pkg/services/alerting/rule_test.go
  45. 10 0
      public/app/core/angular_wrappers.ts
  46. 1 1
      public/app/core/components/PermissionList/AddPermission.tsx
  47. 1 1
      public/app/core/components/PermissionList/DisabledPermissionListItem.tsx
  48. 1 1
      public/app/core/components/PermissionList/PermissionListItem.tsx
  49. 1 1
      public/app/core/components/Select/DataSourcePicker.tsx
  50. 0 25
      public/app/core/components/Select/ResetStyles.tsx
  51. 1 1
      public/app/core/components/Select/TeamPicker.tsx
  52. 1 1
      public/app/core/components/Select/UnitPicker.tsx
  53. 1 1
      public/app/core/components/Select/UserPicker.tsx
  54. 1 1
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  55. 2 4
      public/app/core/components/TagFilter/TagFilter.tsx
  56. 1 3
      public/app/core/core.ts
  57. 3 1
      public/app/core/logs_model.ts
  58. 8 0
      public/app/core/specs/factors.test.ts
  59. 0 94
      public/app/core/utils/colors.ts
  60. 1 1
      public/app/core/utils/explore.ts
  61. 5 0
      public/app/core/utils/factors.ts
  62. 13 4
      public/app/features/alerting/AlertTab.tsx
  63. 0 17
      public/app/features/alerting/AlertTabCtrl.ts
  64. 43 0
      public/app/features/alerting/TestRuleResult.test.tsx
  65. 45 0
      public/app/features/alerting/TestRuleResult.tsx
  66. 7 0
      public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap
  67. 0 14
      public/app/features/alerting/partials/alert_tab.html
  68. 4 3
      public/app/features/annotations/event_manager.ts
  69. 20 1
      public/app/features/dashboard/dashboard_migration.ts
  70. 3 3
      public/app/features/dashboard/dashboard_model.ts
  71. 1 1
      public/app/features/dashboard/dashgrid/EditorTabBody.tsx
  72. 3 9
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  73. 1 2
      public/app/features/dashboard/dashgrid/QueryInspector.tsx
  74. 1 1
      public/app/features/dashboard/panel_model.ts
  75. 11 9
      public/app/features/dashboard/specs/dashboard_migration.test.ts
  76. 3 6
      public/app/features/dashboard/utils/panel.ts
  77. 4 1
      public/app/features/panel/panel_ctrl.ts
  78. 7 2
      public/app/features/panel/partials/general_tab.html
  79. 4 0
      public/app/plugins/datasource/elasticsearch/datasource.ts
  80. 17 4
      public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts
  81. 2 1
      public/app/plugins/datasource/influxdb/query_builder.ts
  82. 6 0
      public/app/plugins/datasource/influxdb/specs/query_builder.test.ts
  83. 3 3
      public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx
  84. 3 3
      public/app/plugins/panel/gauge/GaugePanel.tsx
  85. 8 8
      public/app/plugins/panel/gauge/GaugePanelOptions.tsx
  86. 2 2
      public/app/plugins/panel/gauge/MappingRow.tsx
  87. 4 5
      public/app/plugins/panel/gauge/ValueMappings.test.tsx
  88. 3 4
      public/app/plugins/panel/gauge/ValueMappings.tsx
  89. 4 4
      public/app/plugins/panel/gauge/ValueOptions.tsx
  90. 1 1
      public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx
  91. 2 1
      public/app/plugins/panel/graph/data_processor.ts
  92. 1 1
      public/app/plugins/panel/graph2/GraphPanel.tsx
  93. 2 2
      public/app/routes/GrafanaCtrl.ts
  94. 0 6
      public/app/types/index.ts
  95. 0 31
      public/app/types/panel.ts
  96. 2 2
      public/app/viz/Gauge.test.tsx
  97. 2 2
      public/app/viz/Gauge.tsx
  98. 1 1
      public/app/viz/state/timeSeries.ts
  99. 1 1
      public/dashboards/home.json
  100. 4 6
      public/sass/_grafana.scss

+ 25 - 25
.circleci/config.yml

@@ -127,7 +127,7 @@ jobs:
 
 
   build-all:
   build-all:
     docker:
     docker:
-     - image: grafana/build-container:1.2.1
+     - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout
@@ -200,51 +200,51 @@ jobs:
             - dist/grafana*
             - dist/grafana*
 
 
   grafana-docker-master:
   grafana-docker-master:
-    docker:
-      - image: docker:stable-git
+    machine:
+      image: circleci/classic:201808-01
     steps:
     steps:
       - checkout
       - checkout
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
-      - setup_remote_docker
       - run: docker info
       - 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: 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: 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"
       - run: cd packaging/docker && ./build-enterprise.sh "master"
 
 
 
 
   grafana-docker-pr:
   grafana-docker-pr:
-    docker:
-      - image: docker:stable-git
+    machine:
+      image: circleci/classic:201808-01
     steps:
     steps:
       - checkout
       - checkout
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
-      - setup_remote_docker
       - run: docker info
       - 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}"
       - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
 
 
   grafana-docker-release:
   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:
   build-enterprise:
     docker:
     docker:
-     - image: grafana/build-container:1.2.1
+     - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout
@@ -276,7 +276,7 @@ jobs:
 
 
   build-all-enterprise:
   build-all-enterprise:
     docker:
     docker:
-    - image: grafana/build-container:1.2.1
+    - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
     - checkout
     - 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)
 * **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)
 * **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)
 * **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
 ### Bug fixes
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)

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

+ 2 - 0
build.go

@@ -164,6 +164,8 @@ func makeLatestDistCopies() {
 		"_amd64.deb":          "dist/grafana_latest_amd64.deb",
 		"_amd64.deb":          "dist/grafana_latest_amd64.deb",
 		".x86_64.rpm":         "dist/grafana-latest-1.x86_64.rpm",
 		".x86_64.rpm":         "dist/grafana-latest-1.x86_64.rpm",
 		".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
 		".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 {
 	for _, file := range files {

+ 1 - 0
devenv/docker/blocks/influxdb/influxdb.conf

@@ -69,6 +69,7 @@ reporting-disabled = false
 
 
   unix-socket-enabled = false # enable http service over unix domain socket
   unix-socket-enabled = false # enable http service over unix domain socket
   # bind-socket = "/var/run/influxdb.sock"
   # bind-socket = "/var/run/influxdb.sock"
+  flux-enabled = true
 
 
 [subscriber]
 [subscriber]
   enabled = true
   enabled = true

+ 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": []
     "list": []
   },
   },
   "refresh": "5s",
   "refresh": "5s",
-  "schemaVersion": 16,
+  "schemaVersion": 17,
   "version": 0,
   "version": 0,
   "links": []
   "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
 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
 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.
 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.
 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-highlight-words": "0.11.0",
     "react-popper": "^1.3.0",
     "react-popper": "^1.3.0",
     "react-transition-group": "^2.2.1",
     "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": {
   "devDependencies": {
     "@types/classnames": "^2.2.6",
     "@types/classnames": "^2.2.6",
@@ -33,6 +36,8 @@
     "@types/react": "^16.7.6",
     "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-test-renderer": "^16.0.3",
     "@types/react-test-renderer": "^16.0.3",
+    "@types/tether-drop": "^1.4.8",
+    "@types/tinycolor2": "^1.4.1",
     "react-test-renderer": "^16.7.0",
     "react-test-renderer": "^16.7.0",
     "typescript": "^3.2.2"
     "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 React from 'react';
 import renderer from 'react-test-renderer';
 import renderer from 'react-test-renderer';
-import { ColorPalette } from '../components/colorpicker/ColorPalette';
+import { ColorPalette } from './ColorPalette';
 
 
 describe('CollorPalette', () => {
 describe('CollorPalette', () => {
   it('renders correctly', () => {
   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 React from 'react';
-import { sortedColors } from 'app/core/utils/colors';
+import { sortedColors } from '../../utils';
 
 
 export interface Props {
 export interface Props {
   color: string;
   color: string;
@@ -9,13 +9,13 @@ export interface Props {
 export class ColorPalette extends React.Component<Props, any> {
 export class ColorPalette extends React.Component<Props, any> {
   paletteColors: string[];
   paletteColors: string[];
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.paletteColors = sortedColors;
     this.paletteColors = sortedColors;
     this.onColorSelect = this.onColorSelect.bind(this);
     this.onColorSelect = this.onColorSelect.bind(this);
   }
   }
 
 
-  onColorSelect(color) {
+  onColorSelect(color: string) {
     return () => {
     return () => {
       this.props.onColorSelect(color);
       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 ReactDOM from 'react-dom';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { ColorPickerPopover } from './ColorPickerPopover';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 
 export interface Props {
 export interface Props {
   color: string;
   color: string;
@@ -10,7 +9,7 @@ export interface Props {
 }
 }
 
 
 export class ColorPicker extends React.Component<Props, any> {
 export class ColorPicker extends React.Component<Props, any> {
-  pickerElem: HTMLElement;
+  pickerElem: HTMLElement | null;
   colorPickerDrop: any;
   colorPickerDrop: any;
 
 
   openColorPicker = () => {
   openColorPicker = () => {
@@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
     ReactDOM.render(dropContent, dropContentElem);
     ReactDOM.render(dropContent, dropContentElem);
 
 
     const drop = new Drop({
     const drop = new Drop({
-      target: this.pickerElem,
+      target: this.pickerElem as Element,
       content: dropContentElem,
       content: dropContentElem,
       position: 'top center',
       position: 'top center',
       classes: 'drop-popover',
       classes: 'drop-popover',
@@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
       hoverCloseDelay: 200,
       hoverCloseDelay: 200,
       tetherOptions: {
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+        attachment: 'bottom center',
       },
       },
     });
     });
 
 
@@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
     }, 100);
     }, 100);
   };
   };
 
 
-  onColorSelect = color => {
+  onColorSelect = (color: string) => {
     this.props.onChange(color);
     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> {
 export class ColorPickerPopover extends React.Component<Props, any> {
   pickerNavElem: any;
   pickerNavElem: any;
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
       tab: 'palette',
       tab: 'palette',
@@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
     };
     };
   }
   }
 
 
-  setPickerNavElem(elem) {
+  setPickerNavElem(elem: any) {
     this.pickerNavElem = $(elem);
     this.pickerNavElem = $(elem);
   }
   }
 
 
-  setColor(color) {
+  setColor(color: string) {
     const newColor = tinycolor(color);
     const newColor = tinycolor(color);
     if (newColor.isValid()) {
     if (newColor.isValid()) {
-      this.setState({
-        color: newColor.toString(),
-        colorString: newColor.toString(),
-      });
+      this.setState({ color: newColor.toString(), colorString: newColor.toString() });
       this.props.onColorSelect(color);
       this.props.onColorSelect(color);
     }
     }
   }
   }
 
 
-  sampleColorSelected(color) {
+  sampleColorSelected(color: string) {
     this.setColor(color);
     this.setColor(color);
   }
   }
 
 
-  spectrumColorSelected(color) {
+  spectrumColorSelected(color: any) {
     const rgbColor = color.toRgbString();
     const rgbColor = color.toRgbString();
     this.setColor(rgbColor);
     this.setColor(rgbColor);
   }
   }
 
 
-  onColorStringChange(e) {
+  onColorStringChange(e: any) {
     const colorString = e.target.value;
     const colorString = e.target.value;
-    this.setState({
-      colorString: colorString,
-    });
+    this.setState({ colorString: colorString });
 
 
     const newColor = tinycolor(colorString);
     const newColor = tinycolor(colorString);
     if (newColor.isValid()) {
     if (newColor.isValid()) {
       // Update only color state
       // Update only color state
       const newColorString = newColor.toString();
       const newColorString = newColor.toString();
-      this.setState({
-        color: newColorString,
-      });
+      this.setState({ color: newColorString });
       this.props.onColorSelect(newColorString);
       this.props.onColorSelect(newColorString);
     }
     }
   }
   }
 
 
-  onColorStringBlur(e) {
+  onColorStringBlur(e: any) {
     const colorString = e.target.value;
     const colorString = e.target.value;
     this.setColor(colorString);
     this.setColor(colorString);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
     this.pickerNavElem.find('li:first').addClass('active');
     this.pickerNavElem.find('li:first').addClass('active');
-    this.pickerNavElem.on('show', e => {
+    this.pickerNavElem.on('show', (e: any) => {
       // use href attr (#name => name)
       // use href attr (#name => name)
       const tab = e.target.hash.slice(1);
       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: () => {},
     onToggleAxis: () => {},
   };
   };
 
 
-  constructor(props) {
+  constructor(props: SeriesColorPickerProps) {
     super(props);
     super(props);
   }
   }
 
 
@@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
       remove: true,
       remove: true,
       tetherOptions: {
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
         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 React from 'react';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { ColorPickerPopover } from './ColorPickerPopover';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 
 export interface SeriesColorPickerPopoverProps {
 export interface SeriesColorPickerPopoverProps {
   color: string;
   color: string;
@@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
 
 
 interface AxisSelectorProps {
 interface AxisSelectorProps {
   yaxis: number;
   yaxis: number;
-  onToggleAxis: () => void;
+  onToggleAxis?: () => void;
 }
 }
 
 
 interface AxisSelectorState {
 interface AxisSelectorState {
@@ -30,7 +29,7 @@ interface AxisSelectorState {
 }
 }
 
 
 export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
 export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
-  constructor(props) {
+  constructor(props: AxisSelectorProps) {
     super(props);
     super(props);
     this.state = {
     this.state = {
       yaxis: this.props.yaxis,
       yaxis: this.props.yaxis,
@@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
     this.setState({
     this.setState({
       yaxis: this.state.yaxis === 2 ? 1 : 2,
       yaxis: this.state.yaxis === 2 ? 1 : 2,
     });
     });
-    this.props.onToggleAxis();
+
+    if (this.props.onToggleAxis) {
+      this.props.onToggleAxis();
+    }
   }
   }
 
 
   render() {
   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;
   elem: any;
   isMoving: boolean;
   isMoving: boolean;
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.onSpectrumMove = this.onSpectrumMove.bind(this);
     this.onSpectrumMove = this.onSpectrumMove.bind(this);
     this.setComponentElem = this.setComponentElem.bind(this);
     this.setComponentElem = this.setComponentElem.bind(this);
   }
   }
 
 
-  setComponentElem(elem) {
+  setComponentElem(elem: any) {
     this.elem = $(elem);
     this.elem = $(elem);
   }
   }
 
 
-  onSpectrumMove(color) {
+  onSpectrumMove(color: any) {
     this.isMoving = true;
     this.isMoving = true;
     this.props.onColorSelect(color);
     this.props.onColorSelect(color);
   }
   }
@@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
     this.elem.spectrum('set', this.props.color);
     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
     // 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
     // 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
     // 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>
+);

+ 4 - 1
public/app/core/components/Select/IndicatorsContainer.tsx → packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx

@@ -1,7 +1,10 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
 
 
-export const IndicatorsContainer = props => {
+export const IndicatorsContainer = (props: any) => {
   const isOpen = props.selectProps.menuIsOpen;
   const isOpen = props.selectProps.menuIsOpen;
   return (
   return (
     <components.IndicatorsContainer {...props}>
     <components.IndicatorsContainer {...props}>

+ 4 - 0
public/app/core/components/Select/NoOptionsMessage.tsx → packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx

@@ -1,5 +1,9 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
+// @ts-ignore
 import { OptionProps } from '@torkelo/react-select/lib/components/Option';
 import { OptionProps } from '@torkelo/react-select/lib/components/Option';
 
 
 export interface Props {
 export interface Props {

+ 13 - 8
public/app/core/components/Select/Select.tsx → packages/grafana-ui/src/components/Select/Select.tsx

@@ -1,16 +1,21 @@
 // Libraries
 // Libraries
 import classNames from 'classnames';
 import classNames from 'classnames';
 import React, { PureComponent } from 'react';
 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';
 import { default as ReactSelect } from '@torkelo/react-select';
+// @ts-ignore
 import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
 import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
+// @ts-ignore
 import { components } from '@torkelo/react-select';
 import { components } from '@torkelo/react-select';
 
 
 // Components
 // Components
-import { Option, SingleValue } from './PickerOption';
-import OptionGroup from './OptionGroup';
+import { SelectOption, SingleValue } from './SelectOption';
+import SelectOptionGroup from './SelectOptionGroup';
 import IndicatorsContainer from './IndicatorsContainer';
 import IndicatorsContainer from './IndicatorsContainer';
 import NoOptionsMessage from './NoOptionsMessage';
 import NoOptionsMessage from './NoOptionsMessage';
-import ResetStyles from './ResetStyles';
+import resetSelectStyles from './resetSelectStyles';
 import { CustomScrollbar } from '@grafana/ui';
 import { CustomScrollbar } from '@grafana/ui';
 
 
 export interface SelectOptionItem {
 export interface SelectOptionItem {
@@ -53,7 +58,7 @@ interface AsyncProps {
   loadingMessage?: () => string;
   loadingMessage?: () => string;
 }
 }
 
 
-export const MenuList = props => {
+export const MenuList = (props: any) => {
   return (
   return (
     <components.MenuList {...props}>
     <components.MenuList {...props}>
       <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
       <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
@@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
         classNamePrefix="gf-form-select-box"
         classNamePrefix="gf-form-select-box"
         className={selectClassNames}
         className={selectClassNames}
         components={{
         components={{
-          Option,
+          Option: SelectOption,
           SingleValue,
           SingleValue,
           IndicatorsContainer,
           IndicatorsContainer,
           MenuList,
           MenuList,
-          Group: OptionGroup,
+          Group: SelectOptionGroup,
         }}
         }}
         defaultValue={defaultValue}
         defaultValue={defaultValue}
         value={value}
         value={value}
@@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
         onChange={onChange}
         onChange={onChange}
         options={options}
         options={options}
         placeholder={placeholder || 'Choose'}
         placeholder={placeholder || 'Choose'}
-        styles={ResetStyles}
+        styles={resetSelectStyles()}
         isDisabled={isDisabled}
         isDisabled={isDisabled}
         isLoading={isLoading}
         isLoading={isLoading}
         isClearable={isClearable}
         isClearable={isClearable}
@@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
         isLoading={isLoading}
         isLoading={isLoading}
         defaultOptions={defaultOptions}
         defaultOptions={defaultOptions}
         placeholder={placeholder || 'Choose'}
         placeholder={placeholder || 'Choose'}
-        styles={ResetStyles}
+        styles={resetSelectStyles()}
         loadingMessage={loadingMessage}
         loadingMessage={loadingMessage}
         noOptionsMessage={noOptionsMessage}
         noOptionsMessage={noOptionsMessage}
         isDisabled={isDisabled}
         isDisabled={isDisabled}

+ 22 - 12
public/app/core/components/Select/PickerOption.test.tsx → packages/grafana-ui/src/components/Select/SelectOption.test.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
 import React from 'react';
 import renderer from 'react-test-renderer';
 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<any> = {
   cx: jest.fn(),
   cx: jest.fn(),
   clearValue: jest.fn(),
   clearValue: jest.fn(),
-  onSelect: jest.fn(),
   getStyles: jest.fn(),
   getStyles: jest.fn(),
   getValue: jest.fn(),
   getValue: jest.fn(),
   hasValue: true,
   hasValue: true,
@@ -18,21 +18,31 @@ const model = {
   isFocused: false,
   isFocused: false,
   isSelected: false,
   isSelected: false,
   innerRef: null,
   innerRef: null,
-  innerProps: null,
+  innerProps: {
+    id: '',
+    key: '',
+    onClick: jest.fn(),
+    onMouseOver: jest.fn(),
+    tabIndex: 1,
+  },
   label: 'Option label',
   label: 'Option label',
-  type: null,
+  type: 'option',
   children: 'Model title',
   children: 'Model title',
-  data: {
-    title: 'Model title',
-    imgUrl: 'url/to/avatar',
-    label: 'User picker label',
-  },
   className: 'class-for-user-picker',
   className: 'class-for-user-picker',
 };
 };
 
 
-describe('PickerOption', () => {
+describe('SelectOption', () => {
   it('renders correctly', () => {
   it('renders correctly', () => {
-    const tree = renderer.create(<PickerOption {...model} />).toJSON();
+    const tree = renderer
+      .create(
+        <SelectOption
+          {...model}
+          data={{
+            imgUrl: 'url/to/avatar',
+          }}
+        />
+      )
+      .toJSON();
     expect(tree).toMatchSnapshot();
     expect(tree).toMatchSnapshot();
   });
   });
 });
 });

+ 6 - 3
public/app/core/components/Select/PickerOption.tsx → packages/grafana-ui/src/components/Select/SelectOption.tsx

@@ -1,4 +1,7 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
 import { OptionProps } from 'react-select/lib/components/Option';
 import { OptionProps } from 'react-select/lib/components/Option';
 
 
@@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
   };
   };
 }
 }
 
 
-export const Option = (props: ExtendedOptionProps) => {
+export const SelectOption = (props: ExtendedOptionProps) => {
   const { children, isSelected, data } = props;
   const { children, isSelected, data } = props;
 
 
   return (
   return (
@@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
 };
 };
 
 
 // was not able to type this without typescript error
 // was not able to type this without typescript error
-export const SingleValue = props => {
+export const SingleValue = (props: any) => {
   const { children, data } = props;
   const { children, data } = props;
 
 
   return (
   return (
@@ -41,4 +44,4 @@ export const SingleValue = props => {
   );
   );
 };
 };
 
 
-export default Option;
+export default SelectOption;

+ 2 - 2
public/app/core/components/Select/OptionGroup.tsx → packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx

@@ -9,7 +9,7 @@ interface State {
   expanded: boolean;
   expanded: boolean;
 }
 }
 
 
-export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
+export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
   state = {
   state = {
     expanded: false,
     expanded: false,
   };
   };
@@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
     }
     }
   }
   }
 
 
-  componentDidUpdate(nextProps) {
+  componentDidUpdate(nextProps: ExtendedGroupProps) {
     if (nextProps.selectProps.inputValue !== '') {
     if (nextProps.selectProps.inputValue !== '') {
       this.setState({ expanded: true });
       this.setState({ expanded: true });
     }
     }

+ 0 - 0
public/sass/components/_form_select_box.scss → packages/grafana-ui/src/components/Select/_Select.scss


+ 7 - 2
public/app/core/components/Select/__snapshots__/PickerOption.test.tsx.snap → packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap

@@ -1,7 +1,12 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
-exports[`PickerOption renders correctly 1`] = `
-<div>
+exports[`SelectOption renders correctly 1`] = `
+<div
+  id=""
+  onClick={[MockFunction]}
+  onMouseOver={[MockFunction]}
+  tabIndex={1}
+>
   <div
   <div
     className="gf-form-select-box__desc-option"
     className="gf-form-select-box__desc-option"
   >
   >

+ 27 - 0
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: () => ({}),
+  };
+}

+ 7 - 15
public/app/plugins/panel/gauge/Threshold.test.tsx → packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx

@@ -1,23 +1,18 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 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 setup = (propOverrides?: object) => {
-  const props: PanelOptionsProps<Options> = {
+  const props: Props = {
     onChange: jest.fn(),
     onChange: jest.fn(),
-    options: {
-      ...defaultProps.options,
-      thresholds: [],
-    },
+    thresholds: [],
   };
   };
 
 
   Object.assign(props, propOverrides);
   Object.assign(props, propOverrides);
 
 
-  return shallow(<Thresholds {...props} />).instance() as Thresholds;
+  return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
 };
 };
 
 
 describe('Add threshold', () => {
 describe('Add threshold', () => {
@@ -31,10 +26,7 @@ describe('Add threshold', () => {
 
 
   it('should add another threshold above a first', () => {
   it('should add another threshold above a first', () => {
     const instance = setup({
     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);
     instance.onAddThreshold(1);

+ 41 - 47
public/app/plugins/panel/gauge/Thresholds.tsx → packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -1,32 +1,37 @@
 import React, { PureComponent } from 'react';
 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 {
 interface State {
   thresholds: Threshold[];
   thresholds: Threshold[];
   baseColor: string;
   baseColor: string;
 }
 }
 
 
-export default class Thresholds extends PureComponent<PanelOptionsProps<Options>, State> {
-  constructor(props) {
+export class ThresholdsEditor extends PureComponent<Props, State> {
+  constructor(props: Props) {
     super(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 { thresholds } = this.state;
 
 
     const newThresholds = thresholds.map(threshold => {
     const newThresholds = thresholds.map(threshold => {
       if (threshold.index >= index) {
       if (threshold.index >= index) {
-        threshold = { ...threshold, index: threshold.index + 1 };
+        threshold = {
+          ...threshold,
+          index: threshold.index + 1,
+        };
       }
       }
 
 
       return threshold;
       return threshold;
@@ -48,27 +53,32 @@ export default class Thresholds extends PureComponent<PanelOptionsProps<Options>
     if (index === 0 && thresholds.length === 0) {
     if (index === 0 && thresholds.length === 0) {
       color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
       color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
     } else {
     } 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(
     this.setState(
       {
       {
-        thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]),
+        thresholds: this.sortThresholds([
+          ...newThresholds,
+          {
+            index,
+            value: value as number,
+            color,
+          },
+        ]),
       },
       },
       () => this.updateGauge()
       () => this.updateGauge()
     );
     );
   };
   };
 
 
-  onRemoveThreshold = threshold => {
+  onRemoveThreshold = (threshold: Threshold) => {
     this.setState(
     this.setState(
-      prevState => ({
-        thresholds: prevState.thresholds.filter(t => t !== threshold),
-      }),
+      prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
       () => this.updateGauge()
       () => this.updateGauge()
     );
     );
   };
   };
 
 
-  onChangeThresholdValue = (event, threshold) => {
+  onChangeThresholdValue = (event: any, threshold: Threshold) => {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
 
 
     const newThresholds = thresholds.map(t => {
     const newThresholds = thresholds.map(t => {
@@ -79,12 +89,10 @@ export default class Thresholds extends PureComponent<PanelOptionsProps<Options>
       return t;
       return t;
     });
     });
 
 
-    this.setState({
-      thresholds: newThresholds,
-    });
+    this.setState({ thresholds: newThresholds });
   };
   };
 
 
-  onChangeThresholdColor = (threshold, color) => {
+  onChangeThresholdColor = (threshold: Threshold, color: string) => {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
 
 
     const newThresholds = thresholds.map(t => {
     const newThresholds = thresholds.map(t => {
@@ -103,20 +111,18 @@ export default class Thresholds extends PureComponent<PanelOptionsProps<Options>
     );
     );
   };
   };
 
 
-  onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color });
+  onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
   onBlur = () => {
   onBlur = () => {
-    this.setState(prevState => ({
-      thresholds: this.sortThresholds(prevState.thresholds),
-    }));
+    this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
 
 
     this.updateGauge();
     this.updateGauge();
   };
   };
 
 
   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 thresholds.sort((t1, t2) => {
       return t2.value - t1.value;
       return t2.value - t1.value;
     });
     });
@@ -161,20 +167,8 @@ export default class Thresholds extends PureComponent<PanelOptionsProps<Options>
     return thresholds.map((t, i) => {
     return thresholds.map((t, i) => {
       return (
       return (
         <div key={`${t.value}-${i}`} className="indicator-section">
         <div key={`${t.value}-${i}`} className="indicator-section">
-          <div
-            onClick={() => this.onAddThreshold(t.index + 1)}
-            style={{
-              height: '50%',
-              backgroundColor: t.color,
-            }}
-          />
-          <div
-            onClick={() => this.onAddThreshold(t.index)}
-            style={{
-              height: '50%',
-              backgroundColor: t.color,
-            }}
-          />
+          <div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
+          <div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
         </div>
         </div>
       );
       );
     });
     });
@@ -185,14 +179,14 @@ export default class Thresholds extends PureComponent<PanelOptionsProps<Options>
       <div className="indicator-section" style={{ height: '100%' }}>
       <div className="indicator-section" style={{ height: '100%' }}>
         <div
         <div
           onClick={() => this.onAddThreshold(0)}
           onClick={() => this.onAddThreshold(0)}
-          style={{ height: '100%', backgroundColor: this.props.options.baseColor }}
+          style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
         />
         />
       </div>
       </div>
     );
     );
   }
   }
 
 
   renderBase() {
   renderBase() {
-    const { baseColor } = this.props.options;
+    const baseColor = BasicGaugeColor.Green;
 
 
     return (
     return (
       <div className="threshold-row threshold-row-base">
       <div className="threshold-row threshold-row-base">

+ 0 - 0
public/sass/components/_thresholds.scss → packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss


+ 2 - 0
packages/grafana-ui/src/components/index.scss

@@ -1,3 +1,5 @@
 @import 'CustomScrollbar/CustomScrollbar';
 @import 'CustomScrollbar/CustomScrollbar';
 @import 'DeleteButton/DeleteButton';
 @import 'DeleteButton/DeleteButton';
+@import 'ThresholdsEditor/ThresholdsEditor';
 @import 'Tooltip/Tooltip';
 @import 'Tooltip/Tooltip';
+@import 'Select/Select';

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

@@ -2,3 +2,15 @@ export { DeleteButton } from './DeleteButton/DeleteButton';
 export { Tooltip } from './Tooltip/Tooltip';
 export { Tooltip } from './Tooltip/Tooltip';
 export { Portal } from './Portal/Portal';
 export { Portal } from './Portal/Portal';
 export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
 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';

+ 2 - 2
public/app/plugins/panel/gauge/types.ts → 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;
   baseColor: string;
   decimals: number;
   decimals: number;
   mappings: Array<RangeMap | ValueMap>;
   mappings: Array<RangeMap | ValueMap>;

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

@@ -1,3 +1,4 @@
 export * from './series';
 export * from './series';
 export * from './time';
 export * from './time';
 export * from './panel';
 export * from './panel';
+export * from './gauge';

+ 32 - 0
packages/grafana-ui/src/types/panel.ts

@@ -29,3 +29,35 @@ export interface PanelMenuItem {
   shortcut?: string;
   shortcut?: string;
   subMenu?: PanelMenuItem[];
   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;
+}

+ 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,2 +1,3 @@
 export * from './processTimeSeries';
 export * from './processTimeSeries';
 export * from './valueFormats/valueFormats';
 export * from './valueFormats/valueFormats';
+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"
 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
 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_UID="472"
 ARG GF_GID="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"
 ./push_to_docker_hub.sh "$_grafana_version"
 
 
 if echo "$_grafana_version" | grep -q "^master-"; then
 if echo "$_grafana_version" | grep -q "^master-"; then
-  apk add --no-cache curl
   ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
   ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
 fi
 fi

+ 32 - 8
packaging/docker/build.sh

@@ -1,25 +1,49 @@
 #!/bin/sh
 #!/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 the tag starts with v, treat this as a official release
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
-	_docker_repo=${2:-grafana/grafana}
 else
 else
 	_grafana_version=$_grafana_tag
 	_grafana_version=$_grafana_tag
-	_docker_repo=${2:-grafana/grafana-dev}
 fi
 fi
 
 
 echo "Building ${_docker_repo}:${_grafana_version}"
 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
 # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
-	docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
+	docker_tag_all "${_docker_repo}" "latest"
 else
 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
 fi

+ 29 - 7
packaging/docker/push_to_docker_hub.sh

@@ -1,24 +1,46 @@
 #!/bin/sh
 #!/bin/sh
 set -e
 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 the tag starts with v, treat this as a official release
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
-	_docker_repo=${2:-grafana/grafana}
 else
 else
 	_grafana_version=$_grafana_tag
 	_grafana_version=$_grafana_tag
-	_docker_repo=${2:-grafana/grafana-dev}
 fi
 fi
 
 
+export DOCKER_CLI_EXPERIMENTAL=enabled
+
 echo "pushing ${_docker_repo}:${_grafana_version}"
 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
 if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
 	echo "pushing ${_docker_repo}:latest"
 	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
 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
 fi

+ 1 - 1
pkg/models/dashboards.go

@@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
 func NewDashboardFolder(title string) *Dashboard {
 func NewDashboardFolder(title string) *Dashboard {
 	folder := NewDashboard(title)
 	folder := NewDashboard(title)
 	folder.IsFolder = true
 	folder.IsFolder = true
-	folder.Data.Set("schemaVersion", 16)
+	folder.Data.Set("schemaVersion", 17)
 	folder.Data.Set("version", 0)
 	folder.Data.Set("version", 0)
 	folder.IsFolder = true
 	folder.IsFolder = true
 	return folder
 	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())
 		frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
 		if err != nil {
 		if err != nil {
-			return nil, ValidationError{Reason: "Could not parse frequency"}
+			return nil, ValidationError{Reason: err.Error()}
 		}
 		}
 
 
 		rawFor := jsonAlert.Get("for").MustString()
 		rawFor := jsonAlert.Get("for").MustString()

+ 18 - 3
pkg/services/alerting/rule.go

@@ -1,16 +1,21 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"regexp"
 	"regexp"
 	"strconv"
 	"strconv"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-
 	m "github.com/grafana/grafana/pkg/models"
 	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 {
 type Rule struct {
 	Id                  int64
 	Id                  int64
 	OrgId               int64
 	OrgId               int64
@@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
 	matches := ValueFormatRegex.FindAllString(str, 1)
 	matches := ValueFormatRegex.FindAllString(str, 1)
 
 
 	if len(matches) <= 0 {
 	if len(matches) <= 0 {
-		return 0, fmt.Errorf("Frequency could not be parsed")
+		return 0, ErrFrequencyCouldNotBeParsed
 	}
 	}
 
 
 	value, err := strconv.Atoi(matches[0])
 	value, err := strconv.Atoi(matches[0])
@@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
 		return 0, err
 		return 0, err
 	}
 	}
 
 
+	if value == 0 {
+		return 0, ErrFrequencyCannotBeZeroOrLess
+	}
+
 	unit := UnitFormatRegex.FindAllString(str, 1)[0]
 	unit := UnitFormatRegex.FindAllString(str, 1)[0]
 
 
 	if val, ok := unitMultiplier[unit]; ok {
 	if val, ok := unitMultiplier[unit]; ok {
@@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 	model.PanelId = ruleDef.PanelId
 	model.PanelId = ruleDef.PanelId
 	model.Name = ruleDef.Name
 	model.Name = ruleDef.Name
 	model.Message = ruleDef.Message
 	model.Message = ruleDef.Message
-	model.Frequency = ruleDef.Frequency
 	model.State = ruleDef.State
 	model.State = ruleDef.State
 	model.LastStateChange = ruleDef.NewStateDate
 	model.LastStateChange = ruleDef.NewStateDate
 	model.For = ruleDef.For
 	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.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
 	model.StateChanges = ruleDef.StateChanges
 	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() {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
 		jsonModel := simplejson.NewFromAny(v)
 		id, err := jsonModel.Get("id").Int64()
 		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
 	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) {
 func TestAlertRuleModel(t *testing.T) {
 	Convey("Testing alert rule", t, func() {
 	Convey("Testing alert rule", t, func() {
 
 
@@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) {
 			return &FakeCondition{}, nil
 			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() {
 		Convey("should return err for empty string", func() {
 			_, err := getTimeDurationStringToSeconds("")
 			_, err := getTimeDurationStringToSeconds("")
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)
@@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) {
 				So(len(alertRule.Notifications), ShouldEqual, 2)
 				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 { TagFilter } from './components/TagFilter/TagFilter';
 import { SideMenu } from './components/sidemenu/SideMenu';
 import { SideMenu } from './components/sidemenu/SideMenu';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
+import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
 
 
 export function registerAngularDirectives() {
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@@ -19,4 +20,13 @@ export function registerAngularDirectives() {
     ['onChange', { watchDepth: 'reference' }],
     ['onChange', { watchDepth: 'reference' }],
     ['tagOptions', { watchDepth: 'reference' }],
     ['tagOptions', { watchDepth: 'reference' }],
   ]);
   ]);
+  react2AngularDirective('colorPicker', ColorPicker, [
+    'color',
+    ['onChange', { watchDepth: 'reference', wrapApply: true }],
+  ]);
+  react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
+    'series',
+    'onColorChange',
+    'onToggleAxis',
+  ]);
 }
 }

+ 1 - 1
public/app/core/components/PermissionList/AddPermission.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import { UserPicker } from 'app/core/components/Select/UserPicker';
 import { UserPicker } from 'app/core/components/Select/UserPicker';
 import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
 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 { User } from 'app/types';
 import {
 import {
   dashboardPermissionLevels,
   dashboardPermissionLevels,

+ 1 - 1
public/app/core/components/PermissionList/DisabledPermissionListItem.tsx

@@ -1,5 +1,5 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import Select from 'app/core/components/Select/Select';
+import { Select } from '@grafana/ui';
 import { dashboardPermissionLevels } from 'app/types/acl';
 import { dashboardPermissionLevels } from 'app/types/acl';
 
 
 export interface Props {
 export interface Props {

+ 1 - 1
public/app/core/components/PermissionList/PermissionListItem.tsx

@@ -1,5 +1,5 @@
 import React, { PureComponent } from 'react';
 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 { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
 import { FolderInfo } from 'app/types';
 import { FolderInfo } from 'app/types';
 
 

+ 1 - 1
public/app/core/components/Select/DataSourcePicker.tsx

@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Components
 // Components
-import Select from './Select';
+import { Select } from '@grafana/ui';
 
 
 // Types
 // Types
 import { DataSourceSelectItem } from 'app/types';
 import { DataSourceSelectItem } from 'app/types';

+ 0 - 25
public/app/core/components/Select/ResetStyles.tsx

@@ -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: () => ({}),
-};

+ 1 - 1
public/app/core/components/Select/TeamPicker.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
-import { AsyncSelect } from './Select';
+import { AsyncSelect } from '@grafana/ui';
 import { debounce } from 'lodash';
 import { debounce } from 'lodash';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 
 

+ 1 - 1
public/app/core/components/Select/UnitPicker.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import Select from './Select';
 import { getValueFormats } from '@grafana/ui';
 import { getValueFormats } from '@grafana/ui';
+import { Select } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
   onChange: (item: any) => void;
   onChange: (item: any) => void;

+ 1 - 1
public/app/core/components/Select/UserPicker.tsx

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Components
 // Components
-import { AsyncSelect } from './Select';
+import { AsyncSelect } from '@grafana/ui';
 
 
 // Utils & Services
 // Utils & Services
 import { debounce } from 'lodash';
 import { debounce } from 'lodash';

+ 1 - 1
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
 import { Label } from 'app/core/components/Label/Label';
 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 { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 
 
 import { DashboardSearchHit } from 'app/types';
 import { DashboardSearchHit } from 'app/types';

+ 2 - 4
public/app/core/components/TagFilter/TagFilter.tsx

@@ -1,12 +1,10 @@
 import React from 'react';
 import React from 'react';
+import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
 import AsyncSelect from '@torkelo/react-select/lib/Async';
 import AsyncSelect from '@torkelo/react-select/lib/Async';
 
 
 import { TagOption } from './TagOption';
 import { TagOption } from './TagOption';
 import { TagBadge } from './TagBadge';
 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 { components } from '@torkelo/react-select';
-import ResetStyles from 'app/core/components/Select/ResetStyles';
 
 
 export interface Props {
 export interface Props {
   tags: string[];
   tags: string[];
@@ -51,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
       getOptionValue: i => i.value,
       getOptionValue: i => i.value,
       getOptionLabel: i => i.label,
       getOptionLabel: i => i.label,
       value: tags,
       value: tags,
-      styles: ResetStyles,
+      styles: resetSelectStyles(),
       filterOption: (option, searchQuery) => {
       filterOption: (option, searchQuery) => {
         const regex = RegExp(searchQuery, 'i');
         const regex = RegExp(searchQuery, 'i');
         return regex.test(option.value);
         return regex.test(option.value);

+ 1 - 3
public/app/core/core.ts

@@ -13,11 +13,10 @@ import './partials';
 import './components/jsontree/jsontree';
 import './components/jsontree/jsontree';
 import './components/code_editor/code_editor';
 import './components/code_editor/code_editor';
 import './utils/outline';
 import './utils/outline';
-import './components/colorpicker/ColorPicker';
-import './components/colorpicker/SeriesColorPickerPopover';
 import './components/colorpicker/spectrum_picker';
 import './components/colorpicker/spectrum_picker';
 import './services/search_srv';
 import './services/search_srv';
 import './services/ng_react';
 import './services/ng_react';
+import { colors } from '@grafana/ui/';
 
 
 import { searchDirective } from './components/search/search';
 import { searchDirective } from './components/search/search';
 import { infoPopover } from './components/info_popover';
 import { infoPopover } from './components/info_popover';
@@ -36,7 +35,6 @@ import 'app/core/services/all';
 import './filters/filters';
 import './filters/filters';
 import coreModule from './core_module';
 import coreModule from './core_module';
 import appEvents from './app_events';
 import appEvents from './app_events';
-import colors from './utils/colors';
 import { assignModelProperties } from './utils/model_utils';
 import { assignModelProperties } from './utils/model_utils';
 import { contextSrv } from './services/context_srv';
 import { contextSrv } from './services/context_srv';
 import { KeybindingSrv } from './services/keybindingSrv';
 import { KeybindingSrv } from './services/keybindingSrv';

+ 3 - 1
public/app/core/logs_model.ts

@@ -1,6 +1,8 @@
 import _ from 'lodash';
 import _ from 'lodash';
+import { colors } from '@grafana/ui';
+
 import { TimeSeries } from 'app/core/core';
 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.
  * 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';
 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 {
 export function getThemeColor(dark: string, light: string): string {
   return config.bootData.user.lightTheme ? light : dark;
   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 _ from 'lodash';
+import { colors } from '@grafana/ui';
 
 
 import { renderUrl } from 'app/core/utils/url';
 import { renderUrl } from 'app/core/utils/url';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 import store from 'app/core/store';
 import store from 'app/core/store';
-import colors from 'app/core/utils/colors';
 import { parse as parseDate } from 'app/core/utils/datemath';
 import { parse as parseDate } from 'app/core/utils/datemath';
 
 
 import TimeSeries from 'app/core/time_series2';
 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);
+}

+ 13 - 4
public/app/features/alerting/AlertTab.tsx

@@ -14,6 +14,7 @@ import 'app/features/alerting/AlertTabCtrl';
 // Types
 // Types
 import { DashboardModel } from '../dashboard/dashboard_model';
 import { DashboardModel } from '../dashboard/dashboard_model';
 import { PanelModel } from '../dashboard/panel_model';
 import { PanelModel } from '../dashboard/panel_model';
+import { TestRuleResult } from './TestRuleResult';
 
 
 interface Props {
 interface Props {
   angularPanel?: AngularComponent;
   angularPanel?: AngularComponent;
@@ -65,9 +66,7 @@ export class AlertTab extends PureComponent<Props> {
     const loader = getAngularLoader();
     const loader = getAngularLoader();
     const template = '<alert-tab />';
     const template = '<alert-tab />';
 
 
-    const scopeProps = {
-      ctrl: this.panelCtrl,
-    };
+    const scopeProps = { ctrl: this.panelCtrl };
 
 
     this.component = loader.load(this.element, scopeProps, template);
     this.component = loader.load(this.element, scopeProps, template);
   }
   }
@@ -111,6 +110,16 @@ export class AlertTab extends PureComponent<Props> {
     };
     };
   };
   };
 
 
+  renderTestRuleResult = () => {
+    const { panel, dashboard } = this.props;
+    return <TestRuleResult panelId={panel.id} dashboard={dashboard} />;
+  };
+
+  testRule = (): EditorToolbarView => ({
+    title: 'Test Rule',
+    render: () => this.renderTestRuleResult(),
+  });
+
   onAddAlert = () => {
   onAddAlert = () => {
     this.panelCtrl._enableAlert();
     this.panelCtrl._enableAlert();
     this.component.digest();
     this.component.digest();
@@ -120,7 +129,7 @@ export class AlertTab extends PureComponent<Props> {
   render() {
   render() {
     const { alert } = this.props.panel;
     const { alert } = this.props.panel;
 
 
-    const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : [];
+    const toolbarItems = alert ? [this.stateHistory(), this.testRule(), this.deleteAlert()] : [];
 
 
     const model = {
     const model = {
       title: 'Panel has no alert rule defined',
       title: 'Panel has no alert rule defined',

+ 0 - 17
public/app/features/alerting/AlertTabCtrl.ts

@@ -9,8 +9,6 @@ import appEvents from 'app/core/app_events';
 export class AlertTabCtrl {
 export class AlertTabCtrl {
   panel: any;
   panel: any;
   panelCtrl: any;
   panelCtrl: any;
-  testing: boolean;
-  testResult: any;
   subTabIndex: number;
   subTabIndex: number;
   conditionTypes: any;
   conditionTypes: any;
   alert: 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 */
 /** @ngInject */

+ 43 - 0
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(<TestRuleResult {...props} />);
+
+  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();
+    });
+  });
+});

+ 45 - 0
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<Props, State> {
+  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 <LoadingPlaceholder text="Evaluating rule" />;
+    }
+
+    return <JSONFormatter json={testRuleResponse} />;
+  }
+}

+ 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"
+/>
+`;

+ 0 - 14
public/app/features/alerting/partials/alert_tab.html

@@ -121,20 +121,6 @@
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-
-        <div class="gf-form-button-row">
-          <button class="btn btn-inverse" ng-click="ctrl.test()">
-            Test Rule
-          </button>
-        </div>
-      </div>
-
-      <div class="gf-form-group" ng-if="ctrl.testing">
-        Evaluating rule <i class="fa fa-spinner fa-spin"></i>
-      </div>
-
-      <div class="gf-form-group" ng-if="ctrl.testResult">
-        <json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>

+ 4 - 3
public/app/features/annotations/event_manager.ts

@@ -1,8 +1,6 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import moment from 'moment';
 import moment from 'moment';
 import tinycolor from 'tinycolor2';
 import tinycolor from 'tinycolor2';
-import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { AnnotationEvent } from './event';
 import {
 import {
   OK_COLOR,
   OK_COLOR,
   ALERTING_COLOR,
   ALERTING_COLOR,
@@ -10,7 +8,10 @@ import {
   PENDING_COLOR,
   PENDING_COLOR,
   DEFAULT_ANNOTATION_COLOR,
   DEFAULT_ANNOTATION_COLOR,
   REGION_FILL_ALPHA,
   REGION_FILL_ALPHA,
-} from 'app/core/utils/colors';
+} from '@grafana/ui';
+
+import { MetricsPanelCtrl } from 'app/plugins/sdk';
+import { AnnotationEvent } from './event';
 
 
 export class EventManager {
 export class EventManager {
   event: AnnotationEvent;
   event: AnnotationEvent;

+ 20 - 1
public/app/features/dashboard/dashboard_migration.ts

@@ -9,6 +9,7 @@ import {
 } from 'app/core/constants';
 } from 'app/core/constants';
 import { PanelModel } from './panel_model';
 import { PanelModel } from './panel_model';
 import { DashboardModel } from './dashboard_model';
 import { DashboardModel } from './dashboard_model';
+import getFactors from 'app/core/utils/factors';
 
 
 export class DashboardMigrator {
 export class DashboardMigrator {
   dashboard: DashboardModel;
   dashboard: DashboardModel;
@@ -21,7 +22,7 @@ export class DashboardMigrator {
     let i, j, k, n;
     let i, j, k, n;
     const oldVersion = this.dashboard.schemaVersion;
     const oldVersion = this.dashboard.schemaVersion;
     const panelUpgrades = [];
     const panelUpgrades = [];
-    this.dashboard.schemaVersion = 16;
+    this.dashboard.schemaVersion = 17;
 
 
     if (oldVersion === this.dashboard.schemaVersion) {
     if (oldVersion === this.dashboard.schemaVersion) {
       return;
       return;
@@ -368,6 +369,24 @@ export class DashboardMigrator {
       this.upgradeToGridLayout(old);
       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) {
     if (panelUpgrades.length === 0) {
       return;
       return;
     }
     }

+ 3 - 3
public/app/features/dashboard/dashboard_model.ts

@@ -1,8 +1,8 @@
 import moment from 'moment';
 import moment from 'moment';
 import _ from 'lodash';
 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 { 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 { Emitter } from 'app/core/utils/emitter';
 import { contextSrv } from 'app/core/services/context_srv';
 import { contextSrv } from 'app/core/services/context_srv';
 import sortByKeys from 'app/core/utils/sort_by_keys';
 import sortByKeys from 'app/core/utils/sort_by_keys';
@@ -442,7 +442,7 @@ export class DashboardModel {
     }
     }
 
 
     const selectedOptions = this.getSelectedVariableOptions(variable);
     const selectedOptions = this.getSelectedVariableOptions(variable);
-    const minWidth = panel.minSpan || 6;
+    const maxPerRow = panel.maxPerRow || 4;
     let xPos = 0;
     let xPos = 0;
     let yPos = panel.gridPos.y;
     let yPos = panel.gridPos.y;
 
 
@@ -462,7 +462,7 @@ export class DashboardModel {
       } else {
       } else {
         // set width based on how many are selected
         // set width based on how many are selected
         // assumed the repeated panels should take up full row width
         // 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.x = xPos;
         copy.gridPos.y = yPos;
         copy.gridPos.y = yPos;
 
 

+ 1 - 1
public/app/features/dashboard/dashgrid/EditorTabBody.tsx

@@ -52,7 +52,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
   onToggleToolBarView = (item: EditorToolbarView) => {
   onToggleToolBarView = (item: EditorToolbarView) => {
     this.setState({
     this.setState({
       openView: item,
       openView: item,
-      isOpen: !this.state.isOpen,
+      isOpen: this.state.openView !== item || !this.state.isOpen,
     });
     });
   };
   };
 
 

+ 3 - 9
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -1,10 +1,10 @@
 // Libraries
 // Libraries
-import React, { PureComponent, SFC } from 'react';
+import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Components
 // Components
 import 'app/features/panel/metrics_tab';
 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 { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { QueryInspector } from './QueryInspector';
 import { QueryInspector } from './QueryInspector';
 import { QueryOptions } from './QueryOptions';
 import { QueryOptions } from './QueryOptions';
@@ -36,12 +36,6 @@ interface State {
   isAddingMixed: boolean;
   isAddingMixed: boolean;
 }
 }
 
 
-interface LoadingPlaceholderProps {
-  text: string;
-}
-
-const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
-
 export class QueriesTab extends PureComponent<Props, State> {
 export class QueriesTab extends PureComponent<Props, State> {
   element: HTMLElement;
   element: HTMLElement;
   component: AngularComponent;
   component: AngularComponent;
@@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> {
 
 
   renderQueryInspector = () => {
   renderQueryInspector = () => {
     const { panel } = this.props;
     const { panel } = this.props;
-    return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
+    return <QueryInspector panel={panel} />;
   };
   };
 
 
   renderHelp = () => {
   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 { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
 import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
+import { LoadingPlaceholder } from '@grafana/ui';
 
 
 interface DsQuery {
 interface DsQuery {
   isLoading: boolean;
   isLoading: boolean;
@@ -10,7 +11,6 @@ interface DsQuery {
 
 
 interface Props {
 interface Props {
   panel: any;
   panel: any;
-  LoadingPlaceholder: any;
 }
 }
 
 
 interface State {
 interface State {
@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
 
 
   render() {
   render() {
     const { response, isLoading } = this.state.dsQuery;
     const { response, isLoading } = this.state.dsQuery;
-    const { LoadingPlaceholder } = this.props;
     const { isMocking } = this.state;
     const { isMocking } = this.state;
     const openNodes = this.getNrOfOpenNodes();
     const openNodes = this.getNrOfOpenNodes();
 
 

+ 1 - 1
public/app/features/dashboard/panel_model.ts

@@ -77,7 +77,7 @@ export class PanelModel {
   repeatPanelId?: number;
   repeatPanelId?: number;
   repeatDirection?: string;
   repeatDirection?: string;
   repeatedByRow?: boolean;
   repeatedByRow?: boolean;
-  minSpan?: number;
+  maxPerRow?: number;
   collapsed?: boolean;
   collapsed?: boolean;
   panels?: any;
   panels?: any;
   soloMode?: boolean;
   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', () => {
     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', () => {
     it('graph thresholds should be migrated', () => {
@@ -364,14 +364,6 @@ describe('DashboardModel', () => {
       expect(dashboard.panels.length).toBe(2);
       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', () => {
     it('should assign id', () => {
       model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
       model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
       model.rows[0].panels[0] = {};
       model.rows[0].panels[0] = {};
@@ -380,6 +372,16 @@ describe('DashboardModel', () => {
       expect(dashboard.panels[0].id).toBe(1);
       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[]) {
 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;
     const timeShift = '-' + timeShiftInterpolated;
     newTimeData.timeInfo += ' timeshift ' + timeShift;
     newTimeData.timeInfo += ' timeshift ' + timeShift;
     newTimeData.timeRange = {
     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 config from 'app/core/config';
 import { profiler } from 'app/core/core';
 import { profiler } from 'app/core/core';
 import { Emitter } from 'app/core/core';
 import { Emitter } from 'app/core/core';
+import getFactors from 'app/core/utils/factors';
 import {
 import {
   duplicatePanel,
   duplicatePanel,
   copyPanel as copyPanelUtil,
   copyPanel as copyPanelUtil,
@@ -12,7 +13,7 @@ import {
   sharePanel as sharePanelUtil,
   sharePanel as sharePanelUtil,
 } from 'app/features/dashboard/utils/panel';
 } 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 {
 export class PanelCtrl {
   panel: any;
   panel: any;
@@ -32,6 +33,7 @@ export class PanelCtrl {
   events: Emitter;
   events: Emitter;
   timing: any;
   timing: any;
   loading: boolean;
   loading: boolean;
+  maxPanelsPerRowOptions: number[];
 
 
   constructor($scope, $injector) {
   constructor($scope, $injector) {
     this.$injector = $injector;
     this.$injector = $injector;
@@ -92,6 +94,7 @@ export class PanelCtrl {
     if (!this.editModeInitiated) {
     if (!this.editModeInitiated) {
       this.editModeInitiated = true;
       this.editModeInitiated = true;
       this.events.emit('init-edit-mode', null);
       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>
         </select>
       </div>
       </div>
       <div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
       <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>
           <option value=""></option>
         </select>
         </select>
       </div>
       </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>
   </div>
 </div>
 </div>
 
 

+ 4 - 0
public/app/plugins/datasource/elasticsearch/datasource.ts

@@ -254,6 +254,10 @@ export class ElasticDatasource {
         continue;
         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 queryString = this.templateSrv.replace(target.query || '*', options.scopedVars, 'lucene');
       const queryObj = this.queryBuilder.build(target, adhocFilters, queryString);
       const queryObj = this.queryBuilder.build(target, adhocFilters, queryString);
       const esQuery = angular.toJson(queryObj);
       const esQuery = angular.toJson(queryObj);

+ 17 - 4
public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts

@@ -16,7 +16,13 @@ describe('ElasticDatasource', function(this: any) {
   };
   };
 
 
   const templateSrv = {
   const templateSrv = {
-    replace: jest.fn(text => text),
+    replace: jest.fn(text => {
+      if (text.startsWith("$")) {
+        return `resolvedVariable`;
+      } else {
+        return text;
+      }
+    }),
     getAdhocFilters: jest.fn(() => []),
     getAdhocFilters: jest.fn(() => []),
   };
   };
 
 
@@ -67,7 +73,7 @@ describe('ElasticDatasource', function(this: any) {
   });
   });
 
 
   describe('When issuing metric query with interval pattern', () => {
   describe('When issuing metric query with interval pattern', () => {
-    let requestOptions, parts, header;
+    let requestOptions, parts, header, query;
 
 
     beforeEach(() => {
     beforeEach(() => {
       createDatasource({
       createDatasource({
@@ -81,19 +87,22 @@ describe('ElasticDatasource', function(this: any) {
         return Promise.resolve({ data: { responses: [] } });
         return Promise.resolve({ data: { responses: [] } });
       });
       });
 
 
-      ctx.ds.query({
+      query = {
         range: {
         range: {
           from: moment.utc([2015, 4, 30, 10]),
           from: moment.utc([2015, 4, 30, 10]),
           to: moment.utc([2015, 5, 1, 10]),
           to: moment.utc([2015, 5, 1, 10]),
         },
         },
         targets: [
         targets: [
           {
           {
+            alias: "$varAlias",
             bucketAggs: [],
             bucketAggs: [],
             metrics: [{ type: 'raw_document' }],
             metrics: [{ type: 'raw_document' }],
             query: 'escape\\:test',
             query: 'escape\\:test',
           },
           },
         ],
         ],
-      });
+      };
+
+      ctx.ds.query(query);
 
 
       parts = requestOptions.data.split('\n');
       parts = requestOptions.data.split('\n');
       header = angular.fromJson(parts[0]);
       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']);
       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', () => {
     it('should json escape lucene query', () => {
       const body = angular.fromJson(parts[1]);
       const body = angular.fromJson(parts[1]);
       expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');
       expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');

+ 2 - 1
public/app/plugins/datasource/influxdb/query_builder.ts

@@ -1,4 +1,5 @@
 import _ from 'lodash';
 import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
 
 
 function renderTagCondition(tag, index) {
 function renderTagCondition(tag, index) {
   let str = '';
   let str = '';
@@ -43,7 +44,7 @@ export class InfluxQueryBuilder {
     } else if (type === 'MEASUREMENTS') {
     } else if (type === 'MEASUREMENTS') {
       query = 'SHOW MEASUREMENTS';
       query = 'SHOW MEASUREMENTS';
       if (withMeasurementFilter) {
       if (withMeasurementFilter) {
-        query += ' WITH MEASUREMENT =~ /' + withMeasurementFilter + '/';
+        query += ' WITH MEASUREMENT =~ /' + kbn.regexEscape(withMeasurementFilter) + '/';
       }
       }
     } else if (type === 'FIELDS') {
     } else if (type === 'FIELDS') {
       measurement = this.target.measurement;
       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');
       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', () => {
     it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => {
       const builder = new InfluxQueryBuilder({
       const builder = new InfluxQueryBuilder({
         measurement: '',
         measurement: '',

+ 3 - 3
public/app/plugins/panel/gauge/GaugeOptions.tsx → public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx

@@ -1,10 +1,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import { GaugeOptions, PanelOptionsProps } from '@grafana/ui';
+
 import { Switch } from 'app/core/components/Switch/Switch';
 import { Switch } from 'app/core/components/Switch/Switch';
 import { Label } from '../../../core/components/Label/Label';
 import { Label } from '../../../core/components/Label/Label';
-import { PanelOptionsProps } from '@grafana/ui';
-import { Options } from './types';
 
 
-export default class GaugeOptions extends PureComponent<PanelOptionsProps<Options>> {
+export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
   onToggleThresholdLabels = () =>
   onToggleThresholdLabels = () =>
     this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
     this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
 
 

+ 3 - 3
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -1,10 +1,10 @@
 import React, { PureComponent } from 'react';
 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 { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
 import Gauge from 'app/viz/Gauge';
 import Gauge from 'app/viz/Gauge';
-import { Options } from './types';
 
 
-interface Props extends PanelProps<Options> {}
+interface Props extends PanelProps<GaugeOptions> {}
 
 
 export class GaugePanel extends PureComponent<Props> {
 export class GaugePanel extends PureComponent<Props> {
   render() {
   render() {

+ 8 - 8
public/app/plugins/panel/gauge/GaugePanelOptions.tsx

@@ -1,11 +1,9 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import { BasicGaugeColor, GaugeOptions, PanelOptionsProps, ThresholdsEditor, Threshold } from '@grafana/ui';
+
 import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
 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 ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
-import { Options } from './types';
-import GaugeOptions from './GaugeOptions';
+import GaugeOptionsEditor from './GaugeOptionsEditor';
 
 
 export const defaultProps = {
 export const defaultProps = {
   options: {
   options: {
@@ -24,17 +22,19 @@ export const defaultProps = {
   },
   },
 };
 };
 
 
-export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<Options>> {
+export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
   static defaultProps = defaultProps;
   static defaultProps = defaultProps;
 
 
+  onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds });
+
   render() {
   render() {
     const { onChange, options } = this.props;
     const { onChange, options } = this.props;
     return (
     return (
       <>
       <>
         <div className="form-section">
         <div className="form-section">
           <ValueOptions onChange={onChange} options={options} />
           <ValueOptions onChange={onChange} options={options} />
-          <GaugeOptions onChange={onChange} options={options} />
-          <Thresholds onChange={onChange} options={options} />
+          <GaugeOptionsEditor onChange={onChange} options={options} />
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
         </div>
         </div>
 
 
         <div className="form-section">
         <div className="form-section">

+ 2 - 2
public/app/plugins/panel/gauge/MappingRow.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
+
 import { Label } from 'app/core/components/Label/Label';
 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 {
 interface Props {
   mapping: ValueMap | RangeMap;
   mapping: ValueMap | RangeMap;

+ 4 - 5
public/app/plugins/panel/gauge/ValueMappings.test.tsx

@@ -1,13 +1,12 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 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 { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
 
 
+import ValueMappings from './ValueMappings';
+
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
-  const props: PanelOptionsProps<Options> = {
+  const props: PanelOptionsProps<GaugeOptions> = {
     onChange: jest.fn(),
     onChange: jest.fn(),
     options: {
     options: {
       ...defaultProps.options,
       ...defaultProps.options,

+ 3 - 4
public/app/plugins/panel/gauge/ValueMappings.tsx

@@ -1,15 +1,14 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap } from '@grafana/ui';
+
 import MappingRow from './MappingRow';
 import MappingRow from './MappingRow';
-import { MappingType, RangeMap, ValueMap } from 'app/types';
-import { PanelOptionsProps } from '@grafana/ui';
-import { Options } from './types';
 
 
 interface State {
 interface State {
   mappings: Array<ValueMap | RangeMap>;
   mappings: Array<ValueMap | RangeMap>;
   nextIdToAdd: number;
   nextIdToAdd: number;
 }
 }
 
 
-export default class ValueMappings extends PureComponent<PanelOptionsProps<Options>, State> {
+export default class ValueMappings extends PureComponent<PanelOptionsProps<GaugeOptions>, State> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 

+ 4 - 4
public/app/plugins/panel/gauge/ValueOptions.tsx

@@ -1,9 +1,9 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
+import { GaugeOptions, PanelOptionsProps } from '@grafana/ui';
+
 import { Label } from 'app/core/components/Label/Label';
 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 UnitPicker from 'app/core/components/Select/UnitPicker';
-import { PanelOptionsProps } from '@grafana/ui';
-import { Options } from './types';
 
 
 const statOptions = [
 const statOptions = [
   { value: 'min', label: 'Min' },
   { value: 'min', label: 'Min' },
@@ -21,7 +21,7 @@ const statOptions = [
 
 
 const labelWidth = 6;
 const labelWidth = 6;
 
 
-export default class ValueOptions extends PureComponent<PanelOptionsProps<Options>> {
+export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
   onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
   onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
 
 
   onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
   onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });

+ 1 - 1
public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import { TimeSeries } from 'app/core/core';
 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'];
 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 _ from 'lodash';
+import { colors } from '@grafana/ui';
+
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
-import colors from 'app/core/utils/colors';
 
 
 export class DataProcessor {
 export class DataProcessor {
   constructor(private panel) {}
   constructor(private panel) {}

+ 1 - 1
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -1,7 +1,7 @@
 // Libraries
 // Libraries
 import _ from 'lodash';
 import _ from 'lodash';
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import colors from 'app/core/utils/colors';
+import { colors } from '@grafana/ui';
 
 
 // Utils
 // Utils
 import { processTimeSeries } from '@grafana/ui/src/utils';
 import { processTimeSeries } from '@grafana/ui/src/utils';

+ 2 - 2
public/app/routes/GrafanaCtrl.ts

@@ -1,12 +1,12 @@
 import config from 'app/core/config';
 import config from 'app/core/config';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
+import Drop from 'tether-drop';
+import { colors } from '@grafana/ui';
 
 
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { profiler } from 'app/core/profiler';
 import { profiler } from 'app/core/profiler';
 import appEvents from 'app/core/app_events';
 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 { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
 import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
 import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
 import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';

+ 0 - 6
public/app/types/index.ts

@@ -9,7 +9,6 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { Invitee, OrgUser, User, UsersState, UserState } from './user';
 import { Invitee, OrgUser, User, UsersState, UserState } from './user';
 import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources';
 import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources';
 import { DataQuery, DataQueryResponse, DataQueryOptions } from './series';
 import { DataQuery, DataQueryResponse, DataQueryOptions } from './series';
-import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from './panel';
 import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
 import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
 import { Organization, OrganizationState } from './organization';
 import { Organization, OrganizationState } from './organization';
 import {
 import {
@@ -69,13 +68,8 @@ export {
   AppNotificationTimeout,
   AppNotificationTimeout,
   DashboardSearchHit,
   DashboardSearchHit,
   UserState,
   UserState,
-  Threshold,
   ValidationEvents,
   ValidationEvents,
   ValidationRule,
   ValidationRule,
-  ValueMap,
-  RangeMap,
-  MappingType,
-  BasicGaugeColor,
 };
 };
 
 
 export interface StoreState {
 export interface StoreState {

+ 0 - 31
public/app/types/panel.ts

@@ -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;
-}

+ 2 - 2
public/app/viz/Gauge.test.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
+import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui';
+
 import { Gauge, Props } from './Gauge';
 import { Gauge, Props } from './Gauge';
-import { BasicGaugeColor } from '../types';
-import { TimeSeriesVMs } from '@grafana/ui';
 
 
 jest.mock('jquery', () => ({
 jest.mock('jquery', () => ({
   plot: jest.fn(),
   plot: jest.fn(),

+ 2 - 2
public/app/viz/Gauge.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 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 config from '../core/config';
 import kbn from '../core/utils/kbn';
 import kbn from '../core/utils/kbn';
 
 

+ 1 - 1
public/app/viz/state/timeSeries.ts

@@ -2,7 +2,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Utils
 // Utils
-import colors from 'app/core/utils/colors';
+import { colors } from '@grafana/ui';
 
 
 // Types
 // Types
 import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
 import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';

+ 1 - 1
public/dashboards/home.json

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

+ 4 - 6
public/sass/_grafana.scss

@@ -1,4 +1,4 @@
-// DEPENDENCIES
+ // DEPENDENCIES
 @import '../../node_modules/react-table/react-table.css';
 @import '../../node_modules/react-table/react-table.css';
 
 
 // VENDOR
 // VENDOR
@@ -38,9 +38,6 @@
 @import 'layout/lists';
 @import 'layout/lists';
 @import 'layout/page';
 @import 'layout/page';
 
 
-// LOAD @grafana/ui components
-@import '../../packages/grafana-ui/src/index';
-
 // COMPONENTS
 // COMPONENTS
 @import 'components/scrollbar';
 @import 'components/scrollbar';
 @import 'components/cards';
 @import 'components/cards';
@@ -97,16 +94,17 @@
 @import 'components/page_header';
 @import 'components/page_header';
 @import 'components/dashboard_settings';
 @import 'components/dashboard_settings';
 @import 'components/empty_list_cta';
 @import 'components/empty_list_cta';
-@import 'components/form_select_box';
 @import 'components/panel_editor';
 @import 'components/panel_editor';
 @import 'components/toolbar';
 @import 'components/toolbar';
 @import 'components/add_data_source.scss';
 @import 'components/add_data_source.scss';
 @import 'components/page_loader';
 @import 'components/page_loader';
-@import 'components/thresholds';
 @import 'components/toggle_button_group';
 @import 'components/toggle_button_group';
 @import 'components/value-mappings';
 @import 'components/value-mappings';
 @import 'components/popover-box';
 @import 'components/popover-box';
 
 
+// LOAD @grafana/ui components
+@import '../../packages/grafana-ui/src/index';
+
 // PAGES
 // PAGES
 @import 'pages/login';
 @import 'pages/login';
 @import 'pages/dashboard';
 @import 'pages/dashboard';

Some files were not shown because too many files changed in this diff