From 7699706e65ffd48c03196907de6b89d2b0be1c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Feb 2019 11:23:31 +0100 Subject: [PATCH 01/18] fixed handling of alert urls with true flags, fixes #15454 --- pkg/services/alerting/eval_context.go | 2 +- .../app/features/alerting/AlertRuleItem.tsx | 2 +- .../__snapshots__/AlertRuleItem.test.tsx.snap | 4 +-- .../containers/DashboardPage.test.tsx | 34 ++++++++++++++++++- .../dashboard/containers/DashboardPage.tsx | 6 ++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 4db942e0a55..02b9955662f 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { return c.dashboardRef, nil } -const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d" +const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d" func (c *EvalContext) GetRuleUrl() (string, error) { if c.IsTestRun { diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 86bb0207460..3fec37d19b8 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent { 'fa-pause': rule.state !== 'paused', }); - const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; return (
  • diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap index f686127ebf3..8e076ffd22e 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Render should render component 1`] = ` className="alert-rule-item__name" > { expect(ctx.cleanUpDashboardMock.calls).toBe(1); }); }); + + describe('mapStateToProps with bool fullscreen', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: true, + edit: false, + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(true); + expect(props.urlEdit).toBe(false); + }); + + describe('mapStateToProps with string edit true', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: false, + edit: 'true', + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(false); + expect(props.urlEdit).toBe(true); + }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 27118e297b5..bdb601a692f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ +export const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, urlFolderId: state.location.query.folderId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, + urlFullscreen: !!state.location.query.fullscreen, + urlEdit: !!state.location.query.edit, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, From 2d5fd7fdfd78a930bddff5ae4d48f77f03ee8798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 16 Feb 2019 15:45:19 +0100 Subject: [PATCH 02/18] Fixed prettier issue (#15471) Fixed prettier CI issue that caused build failures --- package.json | 2 +- .../features/explore/LogMessageAnsi.test.tsx | 20 +++++++++++--- .../app/features/explore/LogMessageAnsi.tsx | 27 +++++++++++-------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 0bf3e972a72..70067a166a6 100644 --- a/package.json +++ b/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": { diff --git a/public/app/features/explore/LogMessageAnsi.test.tsx b/public/app/features/explore/LogMessageAnsi.test.tsx index 6560fd7b7dd..8513ed4c114 100644 --- a/public/app/features/explore/LogMessageAnsi.test.tsx +++ b/public/app/features/explore/LogMessageAnsi.test.tsx @@ -16,9 +16,21 @@ describe('', () => { const wrapper = shallow(); 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'); }); }); diff --git a/public/app/features/explore/LogMessageAnsi.tsx b/public/app/features/explore/LogMessageAnsi.tsx index e4df16fa13c..ea751879c2e 100644 --- a/public/app/features/explore/LogMessageAnsi.tsx +++ b/public/app/features/explore/LogMessageAnsi.tsx @@ -46,15 +46,15 @@ export class LogMessageAnsi extends PureComponent { 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 { const { chunks } = this.state; return chunks.map( - (chunk, index) => chunk.style ? - {chunk.text} : - chunk.text + (chunk, index) => + chunk.style ? ( + + {chunk.text} + + ) : ( + chunk.text + ) ); } } From 89ad52598610632f3370add4f0853b9ecacf80ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 08:11:57 +0100 Subject: [PATCH 03/18] Fixed issue with PanelHeader and grid-drag-handle class still being applied in fullscreen, fixes #15480 --- .../features/dashboard/dashgrid/DashboardPanel.tsx | 4 ++-- .../app/features/dashboard/dashgrid/PanelChrome.tsx | 4 +++- .../dashboard/dashgrid/PanelHeader/PanelHeader.tsx | 11 +++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b9c56e36382..bb2470cff17 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent { }; renderReactPanel() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { plugin } = this.state; - return ; + return ; } renderAngularPanel() { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 5a993293946..29fe307e941 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/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 { @@ -193,7 +194,7 @@ export class PanelChrome extends PureComponent { }; 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 { scopedVars={panel.scopedVars} links={panel.links} error={errorMessage} + isFullscreen={isFullscreen} /> {({ error, errorInfo }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 49e32ae058c..0f6563836f0 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/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 { }; 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 { error={error} />
    - {isLoading && ( - - - - )}
    From 4d555aceaa3fb87b6336c35ac49aac1d7b56ef1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Kl=C3=A4rner?= Date: Sun, 17 Feb 2019 13:30:41 +0100 Subject: [PATCH 04/18] Add Lux to units fixes #15479 --- packages/grafana-ui/src/utils/valueFormats/categories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts index efba2cc5b79..e127285b473 100644 --- a/packages/grafana-ui/src/utils/valueFormats/categories.ts +++ b/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') }, ], }, { From b488892f5faf342605bfe1b6a643524a8ec48f1f Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 17 Feb 2019 17:38:56 -0300 Subject: [PATCH 05/18] Added enable_gzip documentation (#15322) --- docs/sources/installation/configuration.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2352bd15b89..f0418ad31a6 100644 --- a/docs/sources/installation/configuration.md +++ b/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`). From 815affe02d95269208fe38f34bcde5a22fdcc519 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 18 Feb 2019 09:39:44 +0100 Subject: [PATCH 06/18] Datasource docs for Loki - adds Loki data source docs to Grafana docs - moved query-related docs from Explore to Loki --- docs/sources/features/datasources/index.md | 2 +- docs/sources/features/datasources/loki.md | 119 +++++++++++++++++++++ docs/sources/features/explore/index.md | 69 ++---------- 3 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 docs/sources/features/datasources/loki.md diff --git a/docs/sources/features/datasources/index.md b/docs/sources/features/datasources/index.md index a892f38a448..42ce3f84819 100644 --- a/docs/sources/features/datasources/index.md +++ b/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" >}}) diff --git a/docs/sources/features/datasources/loki.md b/docs/sources/features/datasources/loki.md new file mode 100644 index 00000000000..d43ef982b4b --- /dev/null +++ b/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. + +
    + +
    + +
    + +### 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 +``` diff --git a/docs/sources/features/explore/index.md b/docs/sources/features/explore/index.md index 25af18c2a3d..bf5811baea6 100644 --- a/docs/sources/features/explore/index.md +++ b/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 - -
    - -
    - -#### 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 From 9e55fef544e35dc8955be9379a88324e91169f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 09:52:31 +0100 Subject: [PATCH 07/18] improved formatting of variable docs --- docs/sources/reference/templating.md | 83 ++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index bf3fbd6a229..825fc5b7ebf 100644 --- a/docs/sources/reference/templating.md +++ b/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'` | (test\.|test2) | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | test.|test2 | 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 varaibles 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). From bf826d7c81ef9db2cd359926d2e32e82c25e6169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 17:35:46 +0100 Subject: [PATCH 08/18] Removed primary class from Add Query button, and changed name of Panel Options tab o General Options --- public/app/features/dashboard/panel_editor/GeneralTab.tsx | 2 +- public/app/features/dashboard/panel_editor/PanelEditor.tsx | 2 +- public/app/features/dashboard/panel_editor/QueriesTab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx index d91737195f1..01a6e39cedb 100644 --- a/public/app/features/dashboard/panel_editor/GeneralTab.tsx +++ b/public/app/features/dashboard/panel_editor/GeneralTab.tsx @@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent { render() { return ( - +
    (this.element = element)} /> ); diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 74870b25f07..1bc42a2fd88 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -45,7 +45,7 @@ interface PanelEditorTab { const panelEditorTabTexts = { [PanelEditorTabIds.Queries]: 'Queries', [PanelEditorTabIds.Visualization]: 'Visualization', - [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Advanced]: 'General', [PanelEditorTabIds.Alert]: 'Alert', }; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index d46ff020906..bef23c03496 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent {
    {!isAddingMixed && ( - )} From 14ba3f58914b8f6e95bf616adb34db3cd75e5d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 10:59:22 +0100 Subject: [PATCH 09/18] Fixed spelling issue in templating docs --- docs/sources/reference/templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 825fc5b7ebf..b00e44943ef 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -106,7 +106,7 @@ Interpolation result: '("test1" OR "test2")' ``` ### Percentencode -Formats single & multi valued varaibles for use in URL parameters. +Formats single & multi valued variables for use in URL parameters. ```bash servers = ['foo()bar BAZ', 'test2'] From 75dd7d00360d337c6b8a5cf0023249cb2e33a8ae Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 18 Feb 2019 11:08:42 +0100 Subject: [PATCH 10/18] Fix typo in view mode cykle button --- public/app/features/dashboard/components/DashNav/DashNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 6db07b5d42e..8806e35c6b6 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -248,7 +248,7 @@ export class DashNav extends PureComponent {
    Date: Mon, 18 Feb 2019 11:33:16 +0100 Subject: [PATCH 11/18] Remove maxDataPoints and interval props from props to remember in panel model --- public/app/features/dashboard/state/PanelModel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 2c0ff674e8a..a58d2c07fa1 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -47,8 +47,6 @@ const mustKeepProps: { [str: string]: boolean } = { timeFrom: true, timeShift: true, hideTimeOverride: true, - maxDataPoints: true, - interval: true, description: true, links: true, fullscreen: true, From abddb442a188101e86d6b160f830527cd8b5afb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 11:41:14 +0100 Subject: [PATCH 12/18] Changed how react panels store their options (#15468) Changed how react panels store their options * Added a ReactPanelPlugin as the interface that react panels export, this way react panels have clearer api, and gives us hooks to handle migrations and a way for panel to handle panel changes in the future * Moved gauge value options into a sub oject and made editor more generic, will be moved out of gauge pane later and shared between singlestat, gauge, bargauge, honecomb * Also remove nested options prop that was there due to bug * Added missing Gauge props * Fixed gauge issue that will require migration later and also value options editor did not handle null decimals or 0 decimals * Fixed unit tests * More fixes for react panels --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 2 +- packages/grafana-ui/src/types/panel.ts | 21 +++++++++- packages/grafana-ui/src/types/plugin.ts | 6 +-- public/app/core/constants.ts | 1 - .../__snapshots__/DashboardPage.test.tsx.snap | 10 ++--- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/DataPanel.tsx | 21 +++++----- .../dashboard/dashgrid/PanelChrome.tsx | 4 +- .../dashgrid/PanelPluginNotFound.tsx | 4 +- .../panel_editor/VisualizationTab.tsx | 28 ++++++-------- .../dashboard/state/DashboardMigrator.test.ts | 2 +- .../dashboard/state/DashboardMigrator.ts | 26 ++++++++++++- .../dashboard/state/PanelModel.test.ts | 14 +++++++ .../features/dashboard/state/PanelModel.ts | 27 +++++-------- ...eOptionsEditor.tsx => GaugeOptionsBox.tsx} | 9 ++++- public/app/plugins/panel/gauge/GaugePanel.tsx | 16 ++++++-- ...ePanelOptions.tsx => GaugePanelEditor.tsx} | 38 +++++++------------ ...eOptions.tsx => SingleStatValueEditor.tsx} | 35 +++++++++++++---- public/app/plugins/panel/gauge/module.tsx | 12 ++++-- public/app/plugins/panel/gauge/types.ts | 30 ++++++++++++--- ...hPanelOptions.tsx => GraphPanelEditor.tsx} | 4 +- public/app/plugins/panel/graph2/module.tsx | 4 +- public/app/plugins/panel/text2/module.tsx | 4 +- 23 files changed, 203 insertions(+), 117 deletions(-) rename public/app/plugins/panel/gauge/{GaugeOptionsEditor.tsx => GaugeOptionsBox.tsx} (85%) rename public/app/plugins/panel/gauge/{GaugePanelOptions.tsx => GaugePanelEditor.tsx} (52%) rename public/app/plugins/panel/gauge/{ValueOptions.tsx => SingleStatValueEditor.tsx} (74%) rename public/app/plugins/panel/graph2/{GraphPanelOptions.tsx => GraphPanelEditor.tsx} (91%) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index a7435a56b3c..b8c257f4138 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -9,7 +9,7 @@ import { Themeable } from '../../index'; type TimeSeriesValue = string | number | null; export interface Props extends Themeable { - decimals: number; + decimals?: number | null; height: number; valueMappings: ValueMapping[]; maxValue: number; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 4eda85f9a28..2da48b0fec6 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,3 +1,4 @@ +import { ComponentClass } from 'react'; import { TimeSeries, LoadingState, TableData } from './data'; import { TimeRange } from './time'; @@ -19,11 +20,29 @@ export interface PanelData { tableData?: TableData; } -export interface PanelOptionsProps { +export interface PanelEditorProps { options: T; onChange: (options: T) => void; } +export class ReactPanelPlugin { + panel: ComponentClass>; + editor?: ComponentClass>; + defaults?: TOptions; + + constructor(panel: ComponentClass>) { + this.panel = panel; + } + + setEditor(editor: ComponentClass>) { + this.editor = editor; + } + + setDefaults(defaults: TOptions) { + this.defaults = defaults; + } +} + export interface PanelSize { width: number; height: number; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index c8f156c08dc..e2dda8ad407 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { PanelProps, PanelOptionsProps } from './panel'; +import { ReactPanelPlugin } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource'; export interface DataSourceApi { @@ -81,9 +81,7 @@ export interface PluginExports { // Panel plugin PanelCtrl?: any; - Panel?: ComponentClass; - PanelOptions?: ComponentClass; - PanelDefaults?: any; + reactPanel: ReactPanelPlugin; } export interface PluginMeta { diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 7d295b27726..d51c4cf83d6 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20; export const PANEL_HEADER_HEIGHT = 27; export const PANEL_BORDER = 2; -export const PANEL_OPTIONS_KEY_PREFIX = 'options-'; diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index 002cac2306e..f60e60c43a8 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index bb2470cff17..9aeddd5a0d9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent { onMouseLeave={this.onMouseLeave} style={styles} > - {plugin.exports.Panel && this.renderReactPanel()} + {plugin.exports.reactPanel && this.renderReactPanel()} {plugin.exports.PanelCtrl && this.renderAngularPanel()}
    )} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 0675c7afa60..9718e150e2a 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -162,7 +162,7 @@ export class DataPanel extends Component { } onError(message, err); - this.setState({ isFirstLoad: false }); + this.setState({ isFirstLoad: false, loading: LoadingState.Error }); } }; @@ -187,7 +187,8 @@ export class DataPanel extends Component { 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 { 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 ( -
    - -
    - ); - } - return null; + return ( +
    + +
    + ); } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 29fe307e941..23c92b23837 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -140,7 +140,7 @@ export class PanelChrome extends PureComponent { renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; - const PanelComponent = plugin.exports.Panel; + const PanelComponent = plugin.exports.reactPanel.panel; // This is only done to increase a counter that is used by backend // image rendering (phantomjs/headless chrome) to know when to capture image @@ -154,7 +154,7 @@ export class PanelChrome extends PureComponent { 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} diff --git a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx index 3f835bdbac2..4067f361f06 100644 --- a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx +++ b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; import { PanelPlugin } from 'app/types'; interface Props { @@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin { }, exports: { - Panel: NotFound, + reactPanel: new ReactPanelPlugin(NotFound), }, }; } diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index f9d8b3df607..8a904961a4f 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent { }; } - 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
    (this.element = element)} />; } - return ( - <> - {PanelOptions ? ( - - ) : ( -

    Visualization has no options

    - )} - - ); + if (plugin.exports.reactPanel) { + const PanelEditor = plugin.exports.reactPanel.editor; + + if (PanelEditor) { + return ; + } + } + + return

    Visualization has no options

    ; } componentDidMount() { diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index fdb309b5db5..e4b29eeddfc 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/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', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index ba631102b81..1aa310308d5 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/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; } diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index a7e112c7ba5..d96838dc640 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -55,5 +55,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); + }); + }); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 2c0ff674e8a..fda586d2776 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,6 @@ 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 { TableData } from '@grafana/ui/src'; @@ -92,6 +91,7 @@ export class PanelModel { timeFrom?: any; timeShift?: any; hideTimeOverride?: any; + options: object; 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) { @@ -134,20 +132,14 @@ export class PanelModel { } 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 +232,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); diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx similarity index 85% rename from public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx rename to public/app/plugins/panel/gauge/GaugeOptionsBox.tsx index 50e2a344a9b..b5d6acca806 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx @@ -1,9 +1,14 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui'; +// Components +import { Switch, PanelOptionsGroup } from '@grafana/ui'; + +// Types +import { FormField, PanelEditorProps } from '@grafana/ui'; import { GaugeOptions } from './types'; -export default class GaugeOptionsEditor extends PureComponent> { +export class GaugeOptionsBox extends PureComponent> { onToggleThresholdLabels = () => this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels }); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 5cb256ee1aa..e7e60a7c417 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -16,9 +16,10 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { const { panelData, width, height, onInterpolate, options } = this.props; + const { valueOptions } = options; - const prefix = onInterpolate(options.prefix); - const suffix = onInterpolate(options.suffix); + const prefix = onInterpolate(valueOptions.prefix); + const suffix = onInterpolate(valueOptions.suffix); let value: TimeSeriesValue; if (panelData.timeSeries) { @@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent { }); if (vmSeries[0]) { - value = vmSeries[0].stats[options.stat]; + value = vmSeries[0].stats[valueOptions.stat]; } else { value = null; } @@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent { {theme => ( )} diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx similarity index 52% rename from public/app/plugins/panel/gauge/GaugePanelOptions.tsx rename to public/app/plugins/panel/gauge/GaugePanelEditor.tsx index 84726ac88bf..63031f9d895 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { - PanelOptionsProps, + PanelEditorProps, ThresholdsEditor, Threshold, PanelOptionsGrid, @@ -8,29 +8,11 @@ import { ValueMapping, } from '@grafana/ui'; -import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import GaugeOptionsEditor from './GaugeOptionsEditor'; -import { GaugeOptions } from './types'; - -export const defaultProps = { - options: { - minValue: 0, - maxValue: 100, - prefix: '', - showThresholdMarkers: true, - showThresholdLabels: false, - suffix: '', - decimals: 0, - stat: 'avg', - unit: 'none', - valueMappings: [], - thresholds: [], - }, -}; - -export default class GaugePanelOptions extends PureComponent> { - static defaultProps = defaultProps; +import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; +import { GaugeOptionsBox } from './GaugeOptionsBox'; +import { GaugeOptions, SingleStatValueOptions } from './types'; +export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, @@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent + this.props.onChange({ + ...this.props.options, + valueOptions, + }); + render() { const { onChange, options } = this.props; return ( <> - - + + diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx similarity index 74% rename from public/app/plugins/panel/gauge/ValueOptions.tsx rename to public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index 1fdccadddf2..86c177bb5e5 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -1,7 +1,12 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Components import UnitPicker from 'app/core/components/Select/UnitPicker'; -import { GaugeOptions } from './types'; +import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Types +import { SingleStatValueOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -19,24 +24,40 @@ const statOptions = [ const labelWidth = 6; -export default class ValueOptions extends PureComponent> { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); +export interface Props { + options: SingleStatValueOptions; + onChange: (valueOptions: SingleStatValueOptions) => void; +} +export class SingleStatValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); onDecimalChange = event => { if (!isNaN(event.target.value)) { - this.props.onChange({ ...this.props.options, decimals: event.target.value }); + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); } }; onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); render() { const { stat, unit, decimals, prefix, suffix } = this.props.options; + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + return (
    @@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 783e4825657..a32cb7cd538 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,4 +1,10 @@ -import GaugePanelOptions, { defaultProps } from './GaugePanelOptions'; -import { GaugePanel } from './GaugePanel'; +import { ReactPanelPlugin } from '@grafana/ui'; -export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults }; +import { GaugePanelEditor } from './GaugePanelEditor'; +import { GaugePanel } from './GaugePanel'; +import { GaugeOptions, defaults } from './types'; + +export const reactPanel = new ReactPanelPlugin(GaugePanel); + +reactPanel.setEditor(GaugePanelEditor); +reactPanel.setDefaults(defaults); diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index 42262178dc8..10dd475eff5 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,15 +1,35 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - decimals: number; valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; showThresholdLabels: boolean; showThresholdMarkers: boolean; - stat: string; - suffix: string; thresholds: Threshold[]; - unit: string; + valueOptions: SingleStatValueOptions; } + +export interface SingleStatValueOptions { + unit: string; + suffix: string; + stat: string; + prefix: string; + decimals?: number | null; +} + +export const defaults: GaugeOptions = { + minValue: 0, + maxValue: 100, + showThresholdMarkers: true, + showThresholdLabels: false, + valueOptions: { + prefix: '', + suffix: '', + decimals: null, + stat: 'avg', + unit: 'none', + }, + valueMappings: [], + thresholds: [], +}; diff --git a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx similarity index 91% rename from public/app/plugins/panel/graph2/GraphPanelOptions.tsx rename to public/app/plugins/panel/graph2/GraphPanelEditor.tsx index a9c2d299589..80b17ccd5c4 100644 --- a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx +++ b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx @@ -3,10 +3,10 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelOptionsProps, Switch } from '@grafana/ui'; +import { PanelEditorProps, Switch } from '@grafana/ui'; import { Options } from './types'; -export class GraphPanelOptions extends PureComponent> { +export class GraphPanelEditor extends PureComponent> { onToggleLines = () => { this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines }); }; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 762d5609541..a3a3fadf6bf 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,4 +1,4 @@ import { GraphPanel } from './GraphPanel'; -import { GraphPanelOptions } from './GraphPanelOptions'; +import { GraphPanelEditor } from './GraphPanelEditor'; -export { GraphPanel as Panel, GraphPanelOptions as PanelOptions }; +export { GraphPanel as Panel, GraphPanelEditor as PanelOptions }; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index cc3ec016273..884a5927a19 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; export class Text2 extends PureComponent { constructor(props: PanelProps) { @@ -11,4 +11,4 @@ export class Text2 extends PureComponent { } } -export { Text2 as Panel }; +export const reactPanel = new ReactPanelPlugin(Text2); From 92972eed7b78a30fbe4573e204288e6774d2cd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Feb 2019 11:40:25 +0100 Subject: [PATCH 13/18] Fixes #15477 --- .../dashboard/state/PanelModel.test.ts | 29 +++++++++++++++++++ .../features/dashboard/state/PanelModel.ts | 21 ++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index d96838dc640..079946b1521 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/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); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index fda586d2776..c96ad57dc1c 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { DataQuery, TimeSeries } from '@grafana/ui'; +import { DataQuery, TimeSeries, Threshold } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; export interface GridPos { @@ -91,7 +91,9 @@ export class PanelModel { timeFrom?: any; timeShift?: any; hideTimeOverride?: any; - options: object; + options: { + [key: string]: any; + }; maxDataPoints?: number; interval?: string; @@ -119,6 +121,8 @@ export class PanelModel { _.defaultsDeep(this, _.cloneDeep(defaults)); // queries must have refId this.ensureQueryIds(); + + this.restoreInfintyForThresholds(); } ensureQueryIds() { @@ -131,6 +135,19 @@ 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.options || {}, panelDefaults); } From 56c965e5df563fa19dc0124fb2f58e7890c3b29b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 18 Feb 2019 13:51:43 +0100 Subject: [PATCH 14/18] cli: chmod 755 for backend plugin binaries Fixes #15500. Does a simple filename check if the binary names ends with _linux_amd64 or _darwin_amd64 then sets the file mode to 755. --- .../grafana-cli/commands/install_command.go | 20 +++++++-- .../commands/install_command_test.go | 41 ++++++++++++++++++ ...18fa4da8096a952608a7e4c7782b4260b41bcf.zip | Bin 0 -> 910 bytes 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index f88bb9bbfff..d758633fea5 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -57,6 +57,8 @@ func installCommand(c CommandLine) error { return InstallPlugin(pluginToInstall, version, c) } +// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API +// and then extracts the zip into the plugins directory. func InstallPlugin(pluginName, version string, c CommandLine) error { pluginFolder := c.PluginDirectory() downloadURL := c.PluginURL() @@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) { return err } + return extractFiles(body, pluginName, filePath) +} + +func extractFiles(body []byte, pluginName string, filePath string) error { r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) if err != nil { return err @@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) { if zf.FileInfo().IsDir() { err := os.Mkdir(newFile, 0777) - if PermissionsError(err) { + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } } else { - dst, err := os.Create(newFile) - if PermissionsError(err) { + fileMode := zf.Mode() + + if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") { + fileMode = os.FileMode(0755) + } + + dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } @@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) { return nil } -func PermissionsError(err error) bool { +func permissionsError(err error) bool { return err != nil && strings.Contains(err.Error(), "permission denied") } diff --git a/pkg/cmd/grafana-cli/commands/install_command_test.go b/pkg/cmd/grafana-cli/commands/install_command_test.go index 52b329adf7f..3554dda82a9 100644 --- a/pkg/cmd/grafana-cli/commands/install_command_test.go +++ b/pkg/cmd/grafana-cli/commands/install_command_test.go @@ -1,6 +1,8 @@ package commands import ( + "io/ioutil" + "os" "testing" . "github.com/smartystreets/goconvey/convey" @@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) { }) }) } + +func TestExtractFiles(t *testing.T) { + Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() { + err := os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + err = os.MkdirAll("testdata/fake-plugins-dir", 0774) + So(err, ShouldBeNil) + + body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip") + So(err, ShouldBeNil) + + err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + //File in zip has permissions 777 + fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 664 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 644 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--") + + //File in zip has permission 755 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + err = os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + }) +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip b/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip new file mode 100644 index 0000000000000000000000000000000000000000..f9263ab3e78a36cf40339c296db344db1ed2e4c7 GIT binary patch literal 910 zcmWIWW@h1H0D((oL4IHclwf6$VMxg=F3}GS;bdTz*_jms!lf1542&!C$rL;z0h zDlqNEnYjfysk#L@rRkY@@#UF$Df#8a@rk)9W+r;66{&F3qe(Gc4!h|&nR%rZ5VMIh z_avI9#bD;<<>x`2tecdXmsnH@HjfMFMv~kq4>J!n6jBn4fB`{_Lm8Rmm~q9A1T>}u z82&nfn9$f_g~S#{OyM?4g#l(11H+O=dzewEZpIRGq}wEiWYgQmx!7$&iABP8V8$WB zH*Xui!tB5jqd>DkF-k&6$)mX6l@S`~s3C Date: Mon, 18 Feb 2019 15:59:37 +0100 Subject: [PATCH 15/18] changelog: adds note for #15500 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b97da0e81c..fe817bde9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 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) + # 6.0.0-beta2 (2019-02-11) ### New Features From 71e74181abc251e4b989d914305bef42149484b2 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 18 Feb 2019 16:38:29 +0100 Subject: [PATCH 16/18] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe817bde9ed..67b77ca0e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### 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) # 6.0.0-beta2 (2019-02-11) From 9738ba82e44dc5cda954cd1acf7dfee279785ccf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 18 Feb 2019 16:54:46 +0100 Subject: [PATCH 17/18] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b77ca0e81..eb072cb496e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ * **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) + # 6.0.0-beta2 (2019-02-11) ### New Features From 3d994b16d0fa66a2cf6d7934c3541a616782aa61 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 18 Feb 2019 16:58:15 +0100 Subject: [PATCH 18/18] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb072cb496e..3d3121df80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ ### 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**: 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)