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

Merge branch 'master' into piechart-react

corpglory-dev 6 лет назад
Родитель
Сommit
54db887cca
46 измененных файлов с 609 добавлено и 232 удалено
  1. 9 0
      CHANGELOG.md
  2. 1 1
      docs/sources/features/datasources/index.md
  3. 119 0
      docs/sources/features/datasources/loki.md
  4. 11 58
      docs/sources/features/explore/index.md
  5. 7 0
      docs/sources/installation/configuration.md
  6. 71 12
      docs/sources/reference/templating.md
  7. 1 1
      package.json
  8. 1 1
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  9. 20 1
      packages/grafana-ui/src/types/panel.ts
  10. 2 4
      packages/grafana-ui/src/types/plugin.ts
  11. 1 0
      packages/grafana-ui/src/utils/valueFormats/categories.ts
  12. 16 4
      pkg/cmd/grafana-cli/commands/install_command.go
  13. 41 0
      pkg/cmd/grafana-cli/commands/install_command_test.go
  14. BIN
      pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip
  15. 1 1
      pkg/services/alerting/eval_context.go
  16. 0 1
      public/app/core/constants.ts
  17. 1 1
      public/app/features/alerting/AlertRuleItem.tsx
  18. 2 2
      public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap
  19. 1 1
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  20. 33 1
      public/app/features/dashboard/containers/DashboardPage.test.tsx
  21. 3 3
      public/app/features/dashboard/containers/DashboardPage.tsx
  22. 5 5
      public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap
  23. 3 3
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  24. 9 12
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  25. 5 3
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  26. 3 8
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  27. 2 2
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  28. 1 1
      public/app/features/dashboard/panel_editor/GeneralTab.tsx
  29. 1 1
      public/app/features/dashboard/panel_editor/PanelEditor.tsx
  30. 1 1
      public/app/features/dashboard/panel_editor/QueriesTab.tsx
  31. 11 17
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  32. 1 1
      public/app/features/dashboard/state/DashboardMigrator.test.ts
  33. 25 1
      public/app/features/dashboard/state/DashboardMigrator.ts
  34. 43 0
      public/app/features/dashboard/state/PanelModel.test.ts
  35. 28 20
      public/app/features/dashboard/state/PanelModel.ts
  36. 16 4
      public/app/features/explore/LogMessageAnsi.test.tsx
  37. 16 11
      public/app/features/explore/LogMessageAnsi.tsx
  38. 7 2
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  39. 12 4
      public/app/plugins/panel/gauge/GaugePanel.tsx
  40. 13 25
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  41. 28 7
      public/app/plugins/panel/gauge/SingleStatValueEditor.tsx
  42. 8 2
      public/app/plugins/panel/gauge/module.tsx
  43. 24 4
      public/app/plugins/panel/gauge/types.ts
  44. 2 2
      public/app/plugins/panel/graph2/GraphPanelEditor.tsx
  45. 2 2
      public/app/plugins/panel/graph2/module.tsx
  46. 2 2
      public/app/plugins/panel/text2/module.tsx

+ 9 - 0
CHANGELOG.md

@@ -1,5 +1,14 @@
 # 6.0.0-beta3 (unreleased)
 
+### Minor
+* **CLI**: Grafana CLI should preserve permissions for backend binaries for Linux and Darwin [#15500](https://github.com/grafana/grafana/issues/15500)
+* **Alerting**: Allow image rendering 90 percent of alertTimeout [#15395](https://github.com/grafana/grafana/pull/15395)
+
+### Bug fixes
+* **Influxdb**: Add support for alerting on InfluxDB queries that use the non_negative_difference function [#15415](https://github.com/grafana/grafana/issues/15415), thx [@kiran3394](https://github.com/kiran3394)
+* **Alerting**: Fix percent_diff calculation when points are nulls [#15443](https://github.com/grafana/grafana/issues/15443), thx [@max-neverov](https://github.com/max-neverov)
+* **Alerting**: Fixed handling of alert urls with true flags [#15454](https://github.com/grafana/grafana/issues/15454)
+
 # 6.0.0-beta2 (2019-02-11)
 
 ### New Features

+ 1 - 1
docs/sources/features/datasources/index.md

@@ -13,7 +13,6 @@ weight = 5
 
 Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes.
 
-
 ## Querying
 
 The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
@@ -28,6 +27,7 @@ The following datasources are officially supported:
 * [InfluxDB]({{< relref "influxdb.md" >}})
 * [OpenTSDB]({{< relref "opentsdb.md" >}})
 * [Prometheus]({{< relref "prometheus.md" >}})
+* [Loki]({{< relref "loki.md" >}})
 * [MySQL]({{< relref "mysql.md" >}})
 * [Postgres]({{< relref "postgres.md" >}})
 * [Microsoft SQL Server (MSSQL)]({{< relref "mssql.md" >}})

+ 119 - 0
docs/sources/features/datasources/loki.md

@@ -0,0 +1,119 @@
++++
+title = "Using Loki in Grafana"
+description = "Guide for using Loki in Grafana"
+keywords = ["grafana", "loki", "logging", "guide"]
+type = "docs"
+aliases = ["/datasources/loki"]
+[menu.docs]
+name = "Loki"
+parent = "datasources"
+weight = 11
++++
+
+# Using Loki in Grafana
+
+> BETA: Querying Loki data requires Grafana's Explore section.
+> Grafana v6.x comes with Explore enabled by default.
+> In Grafana v5.3.x and v5.4.x. you need to enable Explore manually.
+> Viewing Loki data in dashboard panels is not supported yet, but is being worked on.
+
+Grafana ships with built-in support for Loki, Grafana's log aggregation system.
+Just add it as a datasource and you are ready to query your log data in [Explore](/features/explore).
+
+## Adding the data source to Grafana
+
+1. Open Grafana and make sure you are logged in.
+2. In the side menu under the `Configuration` link you should find a link named `Data Sources`.
+3. Click the `Add data source` button at the top.
+4. Select `Loki` from the list of data sources.
+
+> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
+
+| Name            | Description                                                                                                                                   |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| _Name_          | The datasource name. This is how you refer to the datasource in panels, queries, and Explore.                                                 |
+| _Default_       | Default datasource means that it will be pre-selected for new panels.                                                                         |
+| _URL_           | The URL of the Loki instance, e.g., `http://localhost:3100`                                                                                   |
+| _Maximum lines_ | Upper limit for number of log lines returned by Loki (default is 1000). Decrease if your browser is sluggish when displaying logs in Explore. |
+
+## Querying Logs
+
+Querying and displaying log data from Loki is available via [Explore](/features/explore).
+Select the Loki data source, and then enter a log query to display your logs.
+
+> Viewing Loki data in dashboard panels is not supported yet, but is being worked on.
+
+### Log Queries
+
+A log query consists of two parts: **log stream selector**, and a **search expression**. For performance reasons you need to start by choosing a log stream by selecting a log label.
+
+The Logs Explorer (the `Log labels` button) next to the query field shows a list of labels of available log streams. An alternative way to write a query is to use the query field's autocomplete - you start by typing a left curly brace `{` and the autocomplete menu will suggest a list of labels. Press the `enter` key to execute the query.
+
+Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
+
+<div class="medium-6 columns">
+  <video width="800" height="500" controls>
+    <source src="/assets/videos/explore_loki.mp4" type="video/mp4">
+    Your browser does not support the video tag.
+  </video>
+</div>
+
+<br />
+
+### Log Stream Selector
+
+For the label part of the query expression, wrap it in curly braces `{}` and then use the key value syntax for selecting labels. Multiple label expressions are separated by a comma:
+
+`{app="mysql",name="mysql-backup"}`
+
+The following label matching operators are currently supported:
+
+* `=` exactly equal.
+* `!=` not equal.
+* `=~` regex-match.
+* `!~` do not regex-match.
+
+Examples:
+
+* `{name=~"mysql.+"}`
+* `{name!~"mysql.+"}`
+
+The [same rules that apply for Prometheus Label Selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#instant-vector-selectors) apply for Loki Log Stream Selectors.
+
+Another way to add a label selector, is in the table section, clicking on the **Filter** button beside a label will add the label to the query expression. This even works for multiple queries and will the label selector to each query.
+
+### Search Expression
+
+After writing the Log Stream Selector, you can filter the results further by writing a search expression. The search expression can be just text or a regex expression.
+
+Example queries:
+
+* `{job="mysql"} error`
+* `{name="kafka"} tsdb-ops.*io:2003`
+* `{instance=~"kafka-[23]",name="kafka"} kafka.server:type=ReplicaManager`
+
+## Templating
+
+Template variables are not yet supported by Loki.
+
+## Annotations
+
+Annotations are not yet supported by Loki.
+
+## Configure the Datasource with Provisioning
+
+You can set up the datasource via config files with Grafana's provisioning system.
+You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here is an example:
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Loki
+    type: loki
+    url: http://localhost:3100
+    jsonData:
+      maxLines: 1000
+```

+ 11 - 58
docs/sources/features/explore/index.md

@@ -67,9 +67,9 @@ The autocomplete menu can be trigger by pressing Ctrl + Space. The Autocomplete
 
 Suggestions can appear under the query field - click on them to update your query with the suggested change.
 
-- For counters (monotonously increasing metrics), a rate function will be suggested.
-- For buckets, a histogram function will be suggested.
-- For recording rules, possible to expand the rules.
+* For counters (monotonously increasing metrics), a rate function will be suggested.
+* For buckets, a histogram function will be suggested.
+* For recording rules, possible to expand the rules.
 
 ### Table Filters
 
@@ -79,6 +79,8 @@ Click on the filter button <span title="Filter for label" class="logs-label__ico
 
 For Grafana 6.0, the first log integration is for the new open source log aggregation system from Grafana Labs - [Loki](https://github.com/grafana/loki). Loki is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels. Read more about Grafana Loki [here](https://github.com/grafana/loki) or the Grafana Labs hosted variant: [Grafana Cloud Logs](https://grafana.com/loki).
 
+See the [Loki's data source documentation](../datasources/loki) on how to query for log data.
+
 ### Switching from Metrics to Logs
 
 If you switch from a Prometheus query to a logs query (you can do a split first to have your metrics and logs side by side) then it will keep the labels from your query that exist in the logs and use those to query the log streams. For example, the following Prometheus query:
@@ -91,67 +93,18 @@ after switching to the Logs datasource, the query changes to:
 
 This will return a chunk of logs in the selected time range that can be grepped/text searched.
 
-### Log Queries
-
-A log query consists of two parts: **log stream selector**, and a **search expression**. For performance reasons you need to start by choosing a log stream by selecting a log label.
-
-The Logs Explorer (the `Log labels` button) next to the query field shows a list of labels of available log streams. An alternative way to write a query is to use the query field's autocomplete - you start by typing a left curly brace `{` and the autocomplete menu will suggest a list of labels. Press the `enter` key to execute the query.
-
-Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
-
-<div class="medium-6 columns">
-  <video width="800" height="500" controls>
-    <source src="/assets/videos/explore_loki.mp4" type="video/mp4">
-    Your browser does not support the video tag.
-  </video>
-</div>
-
-<br />
-
-#### Log Stream Selector
-
-For the label part of the query expression, wrap it in curly braces `{}` and then use the key value syntax for selecting labels. Multiple label expressions are separated by a comma:
-
-`{app="mysql",name="mysql-backup"}`
-
-The following label matching operators are currently supported:
-
-- `=` exactly equal.
-- `!=` not equal.
-- `=~` regex-match.
-- `!~` do not regex-match.
-
-Examples:
-
-- `{name=~"mysql.+"}`
-- `{name!~"mysql.+"}`
-
-The [same rules that apply for Prometheus Label Selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#instant-vector-selectors) apply for Loki Log Stream Selectors.
-
-Another way to add a label selector, is in the table section, clicking on the **Filter** button beside a label will add the label to the query expression. This even works for multiple queries and will the label selector to each query.
-
-#### Search Expression
-
-After writing the Log Stream Selector, you can filter the results further by writing a search expression. The search expression can be just text or a regex expression.
-
-Example queries:
-
-- `{job="mysql"} error`
-- `{name="kafka"} tsdb-ops.*io:2003`
-- `{instance=~"kafka-[23]",name="kafka"} kafka.server:type=ReplicaManager`
-
 ### Deduping
 
 Log data can be very repetitive and Explore can help by hiding duplicate log lines. There are a few different deduplication algorithms that you can use:
 
-- `exact` Exact matches are done on the whole line, except for date fields.
-- `numbers` Matches on the line after stripping out numbers (durations, IP addresses etc.).
-- `signature` The most aggressive deduping - strips all letters and numbers, and matches on the remaining whitespace and punctuation.
+* `exact` Exact matches are done on the whole line, except for date fields.
+* `numbers` Matches on the line after stripping out numbers (durations, IP addresses etc.).
+* `signature` The most aggressive deduping - strips all letters and numbers, and matches on the remaining whitespace and punctuation.
 
 ### Timestamp, Local time and Labels
 
 There are some other check boxes under the logging graph apart from the Deduping options.
 
-- Timestamp: shows/hides the Timestamp column
-- Local time: shows/hides the Local time column
-- Labels: shows/hides the label filters column
+* Timestamp: shows/hides the Timestamp column
+* Local time: shows/hides the Local time column
+* Labels: shows/hides the label filters column

+ 7 - 0
docs/sources/installation/configuration.md

@@ -160,6 +160,13 @@ The path to the directory where the front end files (HTML, JS, and CSS
 files). Default to `public` which is why the Grafana binary needs to be
 executed with working directory set to the installation path.
 
+### enable_gzip
+
+Set this option to `true` to enable HTTP compression, this can improve 
+transfer speed and bandwidth utilization. It is recommended that most 
+users set it to `true`. By default it is set to `false` for compatibility 
+reasons.
+
 ### cert_file
 
 Path to the certificate file (if `protocol` is set to `https`).

+ 71 - 12
docs/sources/reference/templating.md

@@ -38,22 +38,81 @@ documentation article for details on value escaping during interpolation.
 
 ### Advanced Formatting Options
 
-> Only available in Grafana v5.1+.
-
 The formatting of the variable interpolation depends on the data source but there are some situations where you might want to change the default formatting. For example, the default for the MySql datasource is to join multiple values as comma-separated with quotes: `'server01','server02'`. In some cases you might want to have a comma-separated string without quotes: `server01,server02`. This is now possible with the advanced formatting options.
 
 Syntax: `${var_name:option}`
 
-Filter Option | Example | Raw | Interpolated | Description
------------- | ------------- | ------------- | -------------  | -------------
-`glob` | ${servers:glob} |  `'test1', 'test2'` | `{test1,test2}` | (Default) Formats multi-value variable into a glob (for Graphite queries)
-`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.&#124;test2)</code> | Formats multi-value variable into a regex string
-`pipe` | ${servers:pipe} | `'test.', 'test2'` |  <code>test.&#124;test2</code> | Formats multi-value variable into a pipe-separated string
-`csv`| ${servers:csv} |  `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
-`json`| ${servers:json} |  `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string
-`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
-`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
-`percentencode` | ${servers:percentencode} |  `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
+#### Glob
+Formats multi-value variable into a glob (for Graphite queries).
+
+```bash
+servers = ['test1', 'test2']
+String to interpolate: '${servers:glob}'
+Interpolation result: '{test1,test2}'
+```
+
+### Regex
+Formats multi-value variable into a regex string.
+
+```bash
+servers = ['test1.', 'test2']
+String to interpolate: '${servers:regex}'
+Interpolation result: '(test\.|test2)'
+```
+
+### Pipe
+Formats multi-value variable into a pipe-separated string.
+
+```bash
+servers = ['test1.', 'test2']
+String to interpolate: '${servers:pipe}'
+Interpolation result: 'test.|test2'
+```
+
+### Csv
+Formats multi-value variable as a comma-separated string.
+
+```bash
+servers = ['test1', 'test2']
+String to interpolate: '${servers:csv}'
+Interpolation result: 'test,test2'
+```
+
+### Json
+Formats multi-value variable as a comma-separated string.
+
+```bash
+servers = ['test1', 'test2']
+String to interpolate: '${servers:json}'
+Interpolation result: '["test1", "test2"]'
+```
+
+### Distributed
+Formats multi-value variable in custom format for OpenTSDB.
+
+```bash
+servers = ['test1', 'test2']
+String to interpolate: '${servers:distributed}'
+Interpolation result: 'test1,servers=test2'
+```
+
+### Lucene
+Formats multi-value variable in lucene format for Elasticsearch.
+
+```bash
+servers = ['test1', 'test2']
+String to interpolate: '${servers:lucene}'
+Interpolation result: '("test1" OR "test2")'
+```
+
+### Percentencode
+Formats single & multi valued variables for use in URL parameters.
+
+```bash
+servers = ['foo()bar BAZ', 'test2']
+String to interpolate: '${servers:lucene}'
+Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2'
+```
 
 Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
 

+ 1 - 1
package.json

@@ -121,7 +121,7 @@
     "jest": "jest --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "storybook": "cd packages/grafana-ui && yarn storybook",
-    "prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
+    "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\""
   },
   "husky": {
     "hooks": {

+ 1 - 1
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -9,7 +9,7 @@ import { Themeable } from '../../index';
 type TimeSeriesValue = string | number | null;
 
 export interface Props extends Themeable {
-  decimals: number;
+  decimals?: number | null;
   height: number;
   valueMappings: ValueMapping[];
   maxValue: number;

+ 20 - 1
packages/grafana-ui/src/types/panel.ts

@@ -1,3 +1,4 @@
+import { ComponentClass } from 'react';
 import { TimeSeries, LoadingState, TableData } from './data';
 import { TimeRange } from './time';
 
@@ -19,11 +20,29 @@ export interface PanelData {
   tableData?: TableData;
 }
 
-export interface PanelOptionsProps<T = any> {
+export interface PanelEditorProps<T = any> {
   options: T;
   onChange: (options: T) => void;
 }
 
+export class ReactPanelPlugin<TOptions = any> {
+  panel: ComponentClass<PanelProps<TOptions>>;
+  editor?: ComponentClass<PanelEditorProps<TOptions>>;
+  defaults?: TOptions;
+
+  constructor(panel: ComponentClass<PanelProps<TOptions>>) {
+    this.panel = panel;
+  }
+
+  setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
+    this.editor = editor;
+  }
+
+  setDefaults(defaults: TOptions) {
+    this.defaults = defaults;
+  }
+}
+
 export interface PanelSize {
   width: number;
   height: number;

+ 2 - 4
packages/grafana-ui/src/types/plugin.ts

@@ -1,5 +1,5 @@
 import { ComponentClass } from 'react';
-import { PanelProps, PanelOptionsProps } from './panel';
+import { ReactPanelPlugin } from './panel';
 import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
 
 export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
@@ -81,9 +81,7 @@ export interface PluginExports {
 
   // Panel plugin
   PanelCtrl?: any;
-  Panel?: ComponentClass<PanelProps>;
-  PanelOptions?: ComponentClass<PanelOptionsProps>;
-  PanelDefaults?: any;
+  reactPanel: ReactPanelPlugin;
 }
 
 export interface PluginMeta {

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

@@ -191,6 +191,7 @@ export const getCategories = (): ValueFormatCategory[] => [
       { name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') },
       { name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') },
       { name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') },
+      { name: 'Lux (lx)', id: 'lux', fn: toFixedUnit('lux') },
     ],
   },
   {

+ 16 - 4
pkg/cmd/grafana-cli/commands/install_command.go

@@ -57,6 +57,8 @@ func installCommand(c CommandLine) error {
 	return InstallPlugin(pluginToInstall, version, c)
 }
 
+// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
+// and then extracts the zip into the plugins directory.
 func InstallPlugin(pluginName, version string, c CommandLine) error {
 	pluginFolder := c.PluginDirectory()
 	downloadURL := c.PluginURL()
@@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 		return err
 	}
 
+	return extractFiles(body, pluginName, filePath)
+}
+
+func extractFiles(body []byte, pluginName string, filePath string) error {
 	r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
 	if err != nil {
 		return err
@@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 
 		if zf.FileInfo().IsDir() {
 			err := os.Mkdir(newFile, 0777)
-			if PermissionsError(err) {
+			if permissionsError(err) {
 				return fmt.Errorf(permissionsDeniedMessage, newFile)
 			}
 		} else {
-			dst, err := os.Create(newFile)
-			if PermissionsError(err) {
+			fileMode := zf.Mode()
+
+			if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") {
+				fileMode = os.FileMode(0755)
+			}
+
+			dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
+			if permissionsError(err) {
 				return fmt.Errorf(permissionsDeniedMessage, newFile)
 			}
 
@@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 	return nil
 }
 
-func PermissionsError(err error) bool {
+func permissionsError(err error) bool {
 	return err != nil && strings.Contains(err.Error(), "permission denied")
 }

+ 41 - 0
pkg/cmd/grafana-cli/commands/install_command_test.go

@@ -1,6 +1,8 @@
 package commands
 
 import (
+	"io/ioutil"
+	"os"
 	"testing"
 
 	. "github.com/smartystreets/goconvey/convey"
@@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) {
 		})
 	})
 }
+
+func TestExtractFiles(t *testing.T) {
+	Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() {
+		err := os.RemoveAll("testdata/fake-plugins-dir")
+		So(err, ShouldBeNil)
+
+		err = os.MkdirAll("testdata/fake-plugins-dir", 0774)
+		So(err, ShouldBeNil)
+
+		body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
+		So(err, ShouldBeNil)
+
+		err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir")
+		So(err, ShouldBeNil)
+
+		//File in zip has permissions 777
+		fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64")
+		So(err, ShouldBeNil)
+		So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
+
+		//File in zip has permission 664
+		fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64")
+		So(err, ShouldBeNil)
+		So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
+
+		//File in zip has permission 644
+		fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
+		So(err, ShouldBeNil)
+		So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--")
+
+		//File in zip has permission 755
+		fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary")
+		So(err, ShouldBeNil)
+		So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
+
+		err = os.RemoveAll("testdata/fake-plugins-dir")
+		So(err, ShouldBeNil)
+	})
+}

BIN
pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip


+ 1 - 1
pkg/services/alerting/eval_context.go

@@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
 	return c.dashboardRef, nil
 }
 
-const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
+const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
 
 func (c *EvalContext) GetRuleUrl() (string, error) {
 	if c.IsTestRun {

+ 0 - 1
public/app/core/constants.ts

@@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
 
 export const PANEL_HEADER_HEIGHT = 27;
 export const PANEL_BORDER = 2;
-export const PANEL_OPTIONS_KEY_PREFIX = 'options-';

+ 1 - 1
public/app/features/alerting/AlertRuleItem.tsx

@@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent<Props> {
       'fa-pause': rule.state !== 'paused',
     });
 
-    const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
+    const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
 
     return (
       <li className="alert-rule-item">

+ 2 - 2
public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap

@@ -21,7 +21,7 @@ exports[`Render should render component 1`] = `
         className="alert-rule-item__name"
       >
         <a
-          href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
+          href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
         >
           <Highlighter
             highlightClassName="highlight-search-match"
@@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
     </button>
     <a
       className="btn btn-small btn-inverse alert-list__btn width-2"
-      href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
+      href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
       title="Edit alert rule"
     >
       <i

+ 1 - 1
public/app/features/dashboard/components/DashNav/DashNav.tsx

@@ -248,7 +248,7 @@ export class DashNav extends PureComponent<Props> {
 
         <div className="navbar-buttons navbar-buttons--tv">
           <DashNavButton
-            tooltip="Cycke view mode"
+            tooltip="Cycle view mode"
             classSuffix="tv"
             icon="fa fa-desktop"
             onClick={this.onToggleTVMode}

+ 33 - 1
public/app/features/dashboard/containers/DashboardPage.test.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
-import { DashboardPage, Props, State } from './DashboardPage';
+import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
 import { DashboardModel } from '../state';
 import { cleanUpDashboard } from '../state/actions';
 import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
@@ -250,4 +250,36 @@ describe('DashboardPage', () => {
       expect(ctx.cleanUpDashboardMock.calls).toBe(1);
     });
   });
+
+  describe('mapStateToProps with bool fullscreen', () => {
+    const props = mapStateToProps({
+      location: {
+        routeParams: {},
+        query: {
+          fullscreen: true,
+          edit: false,
+        },
+      },
+      dashboard: {},
+    } as any);
+
+    expect(props.urlFullscreen).toBe(true);
+    expect(props.urlEdit).toBe(false);
+  });
+
+  describe('mapStateToProps with string edit true', () => {
+    const props = mapStateToProps({
+      location: {
+        routeParams: {},
+        query: {
+          fullscreen: false,
+          edit: 'true',
+        },
+      },
+      dashboard: {},
+    } as any);
+
+    expect(props.urlFullscreen).toBe(false);
+    expect(props.urlEdit).toBe(true);
+  });
 });

+ 3 - 3
public/app/features/dashboard/containers/DashboardPage.tsx

@@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent<Props, State> {
   }
 }
 
-const mapStateToProps = (state: StoreState) => ({
+export const mapStateToProps = (state: StoreState) => ({
   urlUid: state.location.routeParams.uid,
   urlSlug: state.location.routeParams.slug,
   urlType: state.location.routeParams.type,
   editview: state.location.query.editview,
   urlPanelId: state.location.query.panelId,
   urlFolderId: state.location.query.folderId,
-  urlFullscreen: state.location.query.fullscreen === true,
-  urlEdit: state.location.query.edit === true,
+  urlFullscreen: !!state.location.query.fullscreen,
+  urlEdit: !!state.location.query.edit,
   initPhase: state.dashboard.initPhase,
   isInitSlow: state.dashboard.isInitSlow,
   initError: state.dashboard.initError,

+ 5 - 5
public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap

@@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed  Should render dashboard grid 1`
         ],
         "refresh": undefined,
         "revision": undefined,
-        "schemaVersion": 17,
+        "schemaVersion": 18,
         "snapshot": undefined,
         "style": "dark",
         "tags": Array [],
@@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed  Should render dashboard grid 1`
               ],
               "refresh": undefined,
               "revision": undefined,
-              "schemaVersion": 17,
+              "schemaVersion": 18,
               "snapshot": undefined,
               "style": "dark",
               "tags": Array [],
@@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
         ],
         "refresh": undefined,
         "revision": undefined,
-        "schemaVersion": 17,
+        "schemaVersion": 18,
         "snapshot": undefined,
         "style": "dark",
         "tags": Array [],
@@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
             ],
             "refresh": undefined,
             "revision": undefined,
-            "schemaVersion": 17,
+            "schemaVersion": 18,
             "snapshot": undefined,
             "style": "dark",
             "tags": Array [],
@@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
               ],
               "refresh": undefined,
               "revision": undefined,
-              "schemaVersion": 17,
+              "schemaVersion": 18,
               "snapshot": undefined,
               "style": "dark",
               "tags": Array [],

+ 3 - 3
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent<Props, State> {
   };
 
   renderReactPanel() {
-    const { dashboard, panel } = this.props;
+    const { dashboard, panel, isFullscreen } = this.props;
     const { plugin } = this.state;
 
-    return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
+    return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} />;
   }
 
   renderAngularPanel() {
@@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
               onMouseLeave={this.onMouseLeave}
               style={styles}
             >
-              {plugin.exports.Panel && this.renderReactPanel()}
+              {plugin.exports.reactPanel && this.renderReactPanel()}
               {plugin.exports.PanelCtrl && this.renderAngularPanel()}
             </div>
           )}

+ 9 - 12
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -162,7 +162,7 @@ export class DataPanel extends Component<Props, State> {
       }
 
       onError(message, err);
-      this.setState({ isFirstLoad: false });
+      this.setState({ isFirstLoad: false, loading: LoadingState.Error });
     }
   };
 
@@ -187,7 +187,8 @@ export class DataPanel extends Component<Props, State> {
     const { loading, isFirstLoad } = this.state;
     const panelData = this.getPanelData();
 
-    if (isFirstLoad && loading === LoadingState.Loading) {
+    // do not render component until we have first data
+    if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
       return this.renderLoadingState();
     }
 
@@ -201,21 +202,17 @@ export class DataPanel extends Component<Props, State> {
 
     return (
       <>
-        {this.renderLoadingState()}
+        {loading === LoadingState.Loading && this.renderLoadingState()}
         {this.props.children({ loading, panelData })}
       </>
     );
   }
 
   private renderLoadingState(): JSX.Element {
-    const { loading } = this.state;
-    if (loading === LoadingState.Loading) {
-      return (
-        <div className="panel-loading">
-          <i className="fa fa-spinner fa-spin" />
-        </div>
-      );
-    }
-    return null;
+    return (
+      <div className="panel-loading">
+        <i className="fa fa-spinner fa-spin" />
+      </div>
+    );
   }
 }

+ 5 - 3
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -29,6 +29,7 @@ export interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
   plugin: PanelPlugin;
+  isFullscreen: boolean;
 }
 
 export interface State {
@@ -139,7 +140,7 @@ export class PanelChrome extends PureComponent<Props, State> {
   renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
-    const PanelComponent = plugin.exports.Panel;
+    const PanelComponent = plugin.exports.reactPanel.panel;
 
     // This is only done to increase a counter that is used by backend
     // image rendering (phantomjs/headless chrome) to know when to capture image
@@ -153,7 +154,7 @@ export class PanelChrome extends PureComponent<Props, State> {
           loading={loading}
           panelData={panelData}
           timeRange={timeRange}
-          options={panel.getOptions(plugin.exports.PanelDefaults)}
+          options={panel.getOptions(plugin.exports.reactPanel.defaults)}
           width={width - 2 * variables.panelhorizontalpadding}
           height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
           renderCounter={renderCounter}
@@ -193,7 +194,7 @@ export class PanelChrome extends PureComponent<Props, State> {
   };
 
   render() {
-    const { dashboard, panel } = this.props;
+    const { dashboard, panel, isFullscreen } = this.props;
     const { errorMessage, timeInfo } = this.state;
     const { transparent } = panel;
 
@@ -216,6 +217,7 @@ export class PanelChrome extends PureComponent<Props, State> {
                 scopedVars={panel.scopedVars}
                 links={panel.links}
                 error={errorMessage}
+                isFullscreen={isFullscreen}
               />
               <ErrorBoundary>
                 {({ error, errorInfo }) => {

+ 3 - 8
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -19,6 +19,7 @@ export interface Props {
   scopedVars?: string;
   links?: [];
   error?: string;
+  isFullscreen: boolean;
 }
 
 interface ClickCoordinates {
@@ -69,10 +70,9 @@ export class PanelHeader extends Component<Props, State> {
   };
 
   render() {
-    const isFullscreen = false;
-    const isLoading = false;
+    const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props;
+
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
-    const { panel, dashboard, timeInfo, scopedVars, error } = this.props;
     const title = templateSrv.replaceWithText(panel.title, scopedVars);
 
     return (
@@ -86,11 +86,6 @@ export class PanelHeader extends Component<Props, State> {
           error={error}
         />
         <div className={panelHeaderClass}>
-          {isLoading && (
-            <span className="panel-loading">
-              <i className="fa fa-spinner fa-spin" />
-            </span>
-          )}
           <div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
             <div className="panel-title">
               <span className="icon-gf panel-alert-icon" />

+ 2 - 2
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import React, { PureComponent } from 'react';
 
 // Types
-import { PanelProps } from '@grafana/ui';
+import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
 import { PanelPlugin } from 'app/types';
 
 interface Props {
@@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
     },
 
     exports: {
-      Panel: NotFound,
+      reactPanel: new ReactPanelPlugin(NotFound),
     },
   };
 }

+ 1 - 1
public/app/features/dashboard/panel_editor/GeneralTab.tsx

@@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
 
   render() {
     return (
-      <EditorTabBody heading="Panel Options" toolbarItems={[]}>
+      <EditorTabBody heading="General" toolbarItems={[]}>
         <div ref={element => (this.element = element)} />
       </EditorTabBody>
     );

+ 1 - 1
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -45,7 +45,7 @@ interface PanelEditorTab {
 const panelEditorTabTexts = {
   [PanelEditorTabIds.Queries]: 'Queries',
   [PanelEditorTabIds.Visualization]: 'Visualization',
-  [PanelEditorTabIds.Advanced]: 'Panel Options',
+  [PanelEditorTabIds.Advanced]: 'General',
   [PanelEditorTabIds.Alert]: 'Alert',
 };
 

+ 1 - 1
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent<Props, State> {
         <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
         <div className="flex-grow-1" />
         {!isAddingMixed && (
-          <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
+          <button className="btn navbar-button" onClick={this.onAddQueryClick}>
             Add Query
           </button>
         )}

+ 11 - 17
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent<Props, State> {
     };
   }
 
-  getPanelDefaultOptions = () => {
+  getReactPanelOptions = () => {
     const { panel, plugin } = this.props;
-
-    if (plugin.exports.PanelDefaults) {
-      return panel.getOptions(plugin.exports.PanelDefaults.options);
-    }
-
-    return panel.getOptions(plugin.exports.PanelDefaults);
+    return panel.getOptions(plugin.exports.reactPanel.defaults);
   };
 
   renderPanelOptions() {
     const { plugin, angularPanel } = this.props;
-    const { PanelOptions } = plugin.exports;
 
     if (angularPanel) {
       return <div ref={element => (this.element = element)} />;
     }
 
-    return (
-      <>
-        {PanelOptions ? (
-          <PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
-        ) : (
-          <p>Visualization has no options</p>
-        )}
-      </>
-    );
+    if (plugin.exports.reactPanel) {
+      const PanelEditor = plugin.exports.reactPanel.editor;
+
+      if (PanelEditor) {
+        return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
+      }
+    }
+
+    return <p>Visualization has no options</p>;
   }
 
   componentDidMount() {

+ 1 - 1
public/app/features/dashboard/state/DashboardMigrator.test.ts

@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
     });
 
     it('dashboard schema version should be set to latest', () => {
-      expect(model.schemaVersion).toBe(17);
+      expect(model.schemaVersion).toBe(18);
     });
 
     it('graph thresholds should be migrated', () => {

+ 25 - 1
public/app/features/dashboard/state/DashboardMigrator.ts

@@ -22,7 +22,7 @@ export class DashboardMigrator {
     let i, j, k, n;
     const oldVersion = this.dashboard.schemaVersion;
     const panelUpgrades = [];
-    this.dashboard.schemaVersion = 17;
+    this.dashboard.schemaVersion = 18;
 
     if (oldVersion === this.dashboard.schemaVersion) {
       return;
@@ -387,6 +387,30 @@ export class DashboardMigrator {
       });
     }
 
+    if (oldVersion < 18) {
+      // migrate change to gauge options
+      panelUpgrades.push(panel => {
+        if (panel['options-gauge']) {
+          panel.options = panel['options-gauge'];
+          panel.options.valueOptions = {
+            unit: panel.options.unit,
+            stat: panel.options.stat,
+            decimals: panel.options.decimals,
+            prefix: panel.options.prefix,
+            suffix: panel.options.suffix,
+          };
+          // this options prop was due to a bug
+          delete panel.options.options;
+          delete panel.options.unit;
+          delete panel.options.stat;
+          delete panel.options.decimals;
+          delete panel.options.prefix;
+          delete panel.options.suffix;
+          delete panel['options-gauge'];
+        }
+      });
+    }
+
     if (panelUpgrades.length === 0) {
       return;
     }

+ 43 - 0
public/app/features/dashboard/state/PanelModel.test.ts

@@ -10,6 +10,20 @@ describe('PanelModel', () => {
         type: 'table',
         showColumns: true,
         targets: [{ refId: 'A' }, { noRefId: true }],
+        options: {
+          thresholds: [
+            {
+              color: '#F2495C',
+              index: 1,
+              value: 50,
+            },
+            {
+              color: '#73BF69',
+              index: 0,
+              value: null,
+            },
+          ],
+        },
       });
     });
 
@@ -35,6 +49,21 @@ describe('PanelModel', () => {
       expect(saveModel.events).toBe(undefined);
     });
 
+    it('should restore -Infinity value for base threshold', () => {
+      expect(model.options.thresholds).toEqual([
+        {
+          color: '#F2495C',
+          index: 1,
+          value: 50,
+        },
+        {
+          color: '#73BF69',
+          index: 0,
+          value: -Infinity,
+        },
+      ]);
+    });
+
     describe('when changing panel type', () => {
       beforeEach(() => {
         model.changeType('graph', true);
@@ -55,5 +84,19 @@ describe('PanelModel', () => {
         expect(model.alert).toBe(undefined);
       });
     });
+
+    describe('get panel options', () => {
+      it('should apply defaults', () => {
+        model.options = { existingProp: 10 };
+        const options = model.getOptions({
+          defaultProp: true,
+          existingProp: 0,
+        });
+
+        expect(options.defaultProp).toBe(true);
+        expect(options.existingProp).toBe(10);
+        expect(model.options).toBe(options);
+      });
+    });
   });
 });

+ 28 - 20
public/app/features/dashboard/state/PanelModel.ts

@@ -3,8 +3,7 @@ import _ from 'lodash';
 
 // Types
 import { Emitter } from 'app/core/utils/emitter';
-import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
-import { DataQuery, TimeSeries } from '@grafana/ui';
+import { DataQuery, TimeSeries, Threshold } from '@grafana/ui';
 import { TableData } from '@grafana/ui/src';
 
 export interface GridPos {
@@ -47,8 +46,6 @@ const mustKeepProps: { [str: string]: boolean } = {
   timeFrom: true,
   timeShift: true,
   hideTimeOverride: true,
-  maxDataPoints: true,
-  interval: true,
   description: true,
   links: true,
   fullscreen: true,
@@ -92,6 +89,9 @@ export class PanelModel {
   timeFrom?: any;
   timeShift?: any;
   hideTimeOverride?: any;
+  options: {
+    [key: string]: any;
+  };
 
   maxDataPoints?: number;
   interval?: string;
@@ -105,8 +105,6 @@ export class PanelModel {
   hasRefreshed: boolean;
   events: Emitter;
   cacheTimeout?: any;
-
-  // cache props between plugins
   cachedPluginOptions?: any;
 
   constructor(model) {
@@ -121,6 +119,8 @@ export class PanelModel {
     _.defaultsDeep(this, _.cloneDeep(defaults));
     // queries must have refId
     this.ensureQueryIds();
+
+    this.restoreInfintyForThresholds();
   }
 
   ensureQueryIds() {
@@ -133,21 +133,28 @@ export class PanelModel {
     }
   }
 
+  restoreInfintyForThresholds() {
+    if (this.options && this.options.thresholds) {
+      this.options.thresholds = this.options.thresholds.map((threshold: Threshold) => {
+        // JSON serialization of -Infinity is 'null' so lets convert it back to -Infinity
+        if (threshold.index === 0 && threshold.value === null) {
+          return { ...threshold, value: -Infinity };
+        }
+
+        return threshold;
+      });
+    }
+  }
+
   getOptions(panelDefaults) {
-    return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
+    return _.defaultsDeep(this.options || {}, panelDefaults);
   }
 
   updateOptions(options: object) {
-    const update: any = {};
-    update[this.getOptionsKey()] = options;
-    Object.assign(this, update);
+    this.options = options;
     this.render();
   }
 
-  private getOptionsKey() {
-    return PANEL_OPTIONS_KEY_PREFIX + this.type;
-  }
-
   getSaveModel() {
     const model: any = {};
     for (const property in this) {
@@ -240,14 +247,15 @@ export class PanelModel {
     // for angular panels only we need to remove all events and let angular panels do some cleanup
     if (fromAngularPanel) {
       this.destroy();
+    }
 
-      for (const key of _.keys(this)) {
-        if (mustKeepProps[key]) {
-          continue;
-        }
-
-        delete this[key];
+    // remove panel type specific  options
+    for (const key of _.keys(this)) {
+      if (mustKeepProps[key]) {
+        continue;
       }
+
+      delete this[key];
     }
 
     this.restorePanelOptions(pluginId);

+ 16 - 4
public/app/features/explore/LogMessageAnsi.test.tsx

@@ -16,9 +16,21 @@ describe('<LogMessageAnsi />', () => {
     const wrapper = shallow(<LogMessageAnsi value={value} />);
 
     expect(wrapper.find('span')).toHaveLength(1);
-    expect(wrapper.find('span').first().prop('style')).toMatchObject(expect.objectContaining({
-      color: expect.any(String)
-    }));
-    expect(wrapper.find('span').first().text()).toBe('ipsum');
+    expect(
+      wrapper
+        .find('span')
+        .first()
+        .prop('style')
+    ).toMatchObject(
+      expect.objectContaining({
+        color: expect.any(String),
+      })
+    );
+    expect(
+      wrapper
+        .find('span')
+        .first()
+        .text()
+    ).toBe('ipsum');
   });
 });

+ 16 - 11
public/app/features/explore/LogMessageAnsi.tsx

@@ -46,15 +46,15 @@ export class LogMessageAnsi extends PureComponent<Props, State> {
     const parsed = ansicolor.parse(props.value);
 
     return {
-      chunks: parsed.spans.map((span) => {
-        return span.css ?
-          {
-            style: convertCSSToStyle(span.css),
-            text: span.text
-          } :
-          { text: span.text };
+      chunks: parsed.spans.map(span => {
+        return span.css
+          ? {
+              style: convertCSSToStyle(span.css),
+              text: span.text,
+            }
+          : { text: span.text };
       }),
-      prevValue: props.value
+      prevValue: props.value,
     };
   }
 
@@ -62,9 +62,14 @@ export class LogMessageAnsi extends PureComponent<Props, State> {
     const { chunks } = this.state;
 
     return chunks.map(
-      (chunk, index) => chunk.style ?
-        <span key={index} style={chunk.style}>{chunk.text}</span> :
-        chunk.text
+      (chunk, index) =>
+        chunk.style ? (
+          <span key={index} style={chunk.style}>
+            {chunk.text}
+          </span>
+        ) : (
+          chunk.text
+        )
     );
   }
 }

+ 7 - 2
public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx → public/app/plugins/panel/gauge/GaugeOptionsBox.tsx

@@ -1,9 +1,14 @@
+// Libraries
 import React, { PureComponent } from 'react';
-import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui';
 
+// Components
+import { Switch, PanelOptionsGroup } from '@grafana/ui';
+
+// Types
+import { FormField, PanelEditorProps } from '@grafana/ui';
 import { GaugeOptions } from './types';
 
-export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
+export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
   onToggleThresholdLabels = () =>
     this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
 

+ 12 - 4
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -16,9 +16,10 @@ interface Props extends PanelProps<GaugeOptions> {}
 export class GaugePanel extends PureComponent<Props> {
   render() {
     const { panelData, width, height, onInterpolate, options } = this.props;
+    const { valueOptions } = options;
 
-    const prefix = onInterpolate(options.prefix);
-    const suffix = onInterpolate(options.suffix);
+    const prefix = onInterpolate(valueOptions.prefix);
+    const suffix = onInterpolate(valueOptions.suffix);
     let value: TimeSeriesValue;
 
     if (panelData.timeSeries) {
@@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent<Props> {
       });
 
       if (vmSeries[0]) {
-        value = vmSeries[0].stats[options.stat];
+        value = vmSeries[0].stats[valueOptions.stat];
       } else {
         value = null;
       }
@@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent<Props> {
         {theme => (
           <Gauge
             value={value}
-            {...this.props.options}
             width={width}
             height={height}
             prefix={prefix}
             suffix={suffix}
+            unit={valueOptions.unit}
+            decimals={valueOptions.decimals}
+            thresholds={options.thresholds}
+            valueMappings={options.valueMappings}
+            showThresholdLabels={options.showThresholdLabels}
+            showThresholdMarkers={options.showThresholdMarkers}
+            minValue={options.minValue}
+            maxValue={options.maxValue}
             theme={theme}
           />
         )}

+ 13 - 25
public/app/plugins/panel/gauge/GaugePanelOptions.tsx → public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import {
-  PanelOptionsProps,
+  PanelEditorProps,
   ThresholdsEditor,
   Threshold,
   PanelOptionsGrid,
@@ -8,29 +8,11 @@ import {
   ValueMapping,
 } from '@grafana/ui';
 
-import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
-import GaugeOptionsEditor from './GaugeOptionsEditor';
-import { GaugeOptions } from './types';
-
-export const defaultProps = {
-  options: {
-    minValue: 0,
-    maxValue: 100,
-    prefix: '',
-    showThresholdMarkers: true,
-    showThresholdLabels: false,
-    suffix: '',
-    decimals: 0,
-    stat: 'avg',
-    unit: 'none',
-    valueMappings: [],
-    thresholds: [],
-  },
-};
-
-export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
-  static defaultProps = defaultProps;
+import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
+import { GaugeOptionsBox } from './GaugeOptionsBox';
+import { GaugeOptions, SingleStatValueOptions } from './types';
 
+export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
   onThresholdsChanged = (thresholds: Threshold[]) =>
     this.props.onChange({
       ...this.props.options,
@@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
       valueMappings,
     });
 
+  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+    this.props.onChange({
+      ...this.props.options,
+      valueOptions,
+    });
+
   render() {
     const { onChange, options } = this.props;
 
     return (
       <>
         <PanelOptionsGrid>
-          <ValueOptions onChange={onChange} options={options} />
-          <GaugeOptionsEditor onChange={onChange} options={options} />
+          <SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
+          <GaugeOptionsBox onChange={onChange} options={options} />
           <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
         </PanelOptionsGrid>
 

+ 28 - 7
public/app/plugins/panel/gauge/ValueOptions.tsx → public/app/plugins/panel/gauge/SingleStatValueEditor.tsx

@@ -1,7 +1,12 @@
+// Libraries
 import React, { PureComponent } from 'react';
-import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
+
+// Components
 import UnitPicker from 'app/core/components/Select/UnitPicker';
-import { GaugeOptions } from './types';
+import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
+
+// Types
+import { SingleStatValueOptions } from './types';
 
 const statOptions = [
   { value: 'min', label: 'Min' },
@@ -19,24 +24,40 @@ const statOptions = [
 
 const labelWidth = 6;
 
-export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
-  onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
+export interface Props {
+  options: SingleStatValueOptions;
+  onChange: (valueOptions: SingleStatValueOptions) => void;
+}
 
+export class SingleStatValueEditor extends PureComponent<Props> {
+  onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
   onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
 
   onDecimalChange = event => {
     if (!isNaN(event.target.value)) {
-      this.props.onChange({ ...this.props.options, decimals: event.target.value });
+      this.props.onChange({
+        ...this.props.options,
+        decimals: parseInt(event.target.value, 10),
+      });
+    } else {
+      this.props.onChange({
+        ...this.props.options,
+        decimals: null,
+      });
     }
   };
 
   onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
-
   onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
 
   render() {
     const { stat, unit, decimals, prefix, suffix } = this.props.options;
 
+    let decimalsString = '';
+    if (Number.isFinite(decimals)) {
+      decimalsString = decimals.toString();
+    }
+
     return (
       <PanelOptionsGroup title="Value">
         <div className="gf-form">
@@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
           labelWidth={labelWidth}
           placeholder="auto"
           onChange={this.onDecimalChange}
-          value={decimals || ''}
+          value={decimalsString}
           type="number"
         />
         <FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />

+ 8 - 2
public/app/plugins/panel/gauge/module.tsx

@@ -1,4 +1,10 @@
-import GaugePanelOptions, { defaultProps } from './GaugePanelOptions';
+import { ReactPanelPlugin } from '@grafana/ui';
+
+import { GaugePanelEditor } from './GaugePanelEditor';
 import { GaugePanel } from './GaugePanel';
+import { GaugeOptions, defaults } from './types';
+
+export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
 
-export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults };
+reactPanel.setEditor(GaugePanelEditor);
+reactPanel.setDefaults(defaults);

+ 24 - 4
public/app/plugins/panel/gauge/types.ts

@@ -1,15 +1,35 @@
 import { Threshold, ValueMapping } from '@grafana/ui';
 
 export interface GaugeOptions {
-  decimals: number;
   valueMappings: ValueMapping[];
   maxValue: number;
   minValue: number;
-  prefix: string;
   showThresholdLabels: boolean;
   showThresholdMarkers: boolean;
-  stat: string;
-  suffix: string;
   thresholds: Threshold[];
+  valueOptions: SingleStatValueOptions;
+}
+
+export interface SingleStatValueOptions {
   unit: string;
+  suffix: string;
+  stat: string;
+  prefix: string;
+  decimals?: number | null;
 }
+
+export const defaults: GaugeOptions = {
+  minValue: 0,
+  maxValue: 100,
+  showThresholdMarkers: true,
+  showThresholdLabels: false,
+  valueOptions: {
+    prefix: '',
+    suffix: '',
+    decimals: null,
+    stat: 'avg',
+    unit: 'none',
+  },
+  valueMappings: [],
+  thresholds: [],
+};

+ 2 - 2
public/app/plugins/panel/graph2/GraphPanelOptions.tsx → public/app/plugins/panel/graph2/GraphPanelEditor.tsx

@@ -3,10 +3,10 @@ import _ from 'lodash';
 import React, { PureComponent } from 'react';
 
 // Types
-import { PanelOptionsProps, Switch } from '@grafana/ui';
+import { PanelEditorProps, Switch } from '@grafana/ui';
 import { Options } from './types';
 
-export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {
+export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
   onToggleLines = () => {
     this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
   };

+ 2 - 2
public/app/plugins/panel/graph2/module.tsx

@@ -1,4 +1,4 @@
 import { GraphPanel } from './GraphPanel';
-import { GraphPanelOptions } from './GraphPanelOptions';
+import { GraphPanelEditor } from './GraphPanelEditor';
 
-export { GraphPanel as Panel, GraphPanelOptions as PanelOptions };
+export { GraphPanel as Panel, GraphPanelEditor as PanelOptions };

+ 2 - 2
public/app/plugins/panel/text2/module.tsx

@@ -1,5 +1,5 @@
 import React, { PureComponent } from 'react';
-import { PanelProps } from '@grafana/ui';
+import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
 
 export class Text2 extends PureComponent<PanelProps> {
   constructor(props: PanelProps) {
@@ -11,4 +11,4 @@ export class Text2 extends PureComponent<PanelProps> {
   }
 }
 
-export { Text2 as Panel };
+export const reactPanel = new ReactPanelPlugin(Text2);