Commit 8838e430 by Torkel Ödegaard

Merge branch 'master' of github.com:grafana/grafana

parents 9d5e4bee 7583b78c
...@@ -11,7 +11,11 @@ ...@@ -11,7 +11,11 @@
## New Features ## New Features
* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson) * **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
* **Postgres/MySQL**: add __timeGroup macro for mysql [#9596](https://github.com/grafana/grafana/pull/9596), thanks [@svenklemm](https://github.com/svenklemm)
* **Text**: Text panel are now edited in the ace editor. [#9698](https://github.com/grafana/grafana/pull/9698), thx [@mtanda](https://github.com/mtanda)
## Minor
* **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend)
## Tech ## Tech
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
...@@ -21,6 +25,10 @@ ...@@ -21,6 +25,10 @@
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu) * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
# 4.6.1 (unreleased)
* **Singlestat**: Lost thresholds when using save dashboard as [#9681](https://github.com/grafana/grafana/issues/9681)
# 4.6.0 (2017-10-26) # 4.6.0 (2017-10-26)
## Fixes ## Fixes
......
...@@ -46,8 +46,8 @@ those options. ...@@ -46,8 +46,8 @@ those options.
- [Graphite]({{< relref "features/datasources/graphite.md" >}}) - [Graphite]({{< relref "features/datasources/graphite.md" >}})
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) - [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) - [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
- [Prometheus]({{< relref "features/datasources/influxdb.md" >}}) - [Prometheus]({{< relref "features/datasources/prometheus.md" >}})
- [OpenTSDB]({{< relref "features/datasources/prometheus.md" >}}) - [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
- [MySQL]({{< relref "features/datasources/mysql.md" >}}) - [MySQL]({{< relref "features/datasources/mysql.md" >}})
- [Postgres]({{< relref "features/datasources/postgres.md" >}}) - [Postgres]({{< relref "features/datasources/postgres.md" >}})
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}}) - [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})
...@@ -225,7 +225,7 @@ func init() { ...@@ -225,7 +225,7 @@ func init() {
M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{ M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{
Name: "api_dataproxy_request_all_milliseconds", Name: "api_dataproxy_request_all_milliseconds",
Help: "summary for dashboard search duration", Help: "summary for dataproxy request duration",
Namespace: exporterName, Namespace: exporterName,
}) })
......
...@@ -3,6 +3,8 @@ package mysql ...@@ -3,6 +3,8 @@ package mysql
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
) )
...@@ -25,7 +27,7 @@ func (m *MySqlMacroEngine) Interpolate(timeRange *tsdb.TimeRange, sql string) (s ...@@ -25,7 +27,7 @@ func (m *MySqlMacroEngine) Interpolate(timeRange *tsdb.TimeRange, sql string) (s
var macroError error var macroError error
sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
res, err := m.evaluateMacro(groups[1], groups[2:]) res, err := m.evaluateMacro(groups[1], strings.Split(groups[2], ","))
if err != nil && macroError == nil { if err != nil && macroError == nil {
macroError = err macroError = err
return "macro_error()" return "macro_error()"
...@@ -73,6 +75,15 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er ...@@ -73,6 +75,15 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
case "__timeTo": case "__timeTo":
return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeGroup":
if len(args) != 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
interval, err := time.ParseDuration(strings.Trim(args[1], `'" `))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
return fmt.Sprintf("cast(cast(UNIX_TIMESTAMP(%s)/(%.0f) as signed)*%.0f as signed)", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter": case "__unixEpochFilter":
if len(args) == 0 { if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name) return "", fmt.Errorf("missing time column argument for macro %v", name)
......
...@@ -40,6 +40,14 @@ func TestMacroEngine(t *testing.T) { ...@@ -40,6 +40,14 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914186738)") So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914186738)")
}) })
Convey("interpolate __timeGroup function", func() {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
})
Convey("interpolate __timeTo function", func() { Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(timeRange, "select $__timeTo(time_column)") sql, err := engine.Interpolate(timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil) So(err, ShouldBeNil)
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
) )
...@@ -80,10 +81,14 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, ...@@ -80,10 +81,14 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
case "__timeTo": case "__timeTo":
return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeGroup": case "__timeGroup":
if len(args) < 2 { if len(args) != 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name) return "", fmt.Errorf("macro %v needs time column and interval", name)
} }
return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int*extract(epoch from %s::interval)", args[0], args[1], args[1]), nil interval, err := time.ParseDuration(strings.Trim(args[1], `' `))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
return fmt.Sprintf("(extract(epoch from \"%s\")/%v)::bigint*%v", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter": case "__unixEpochFilter":
if len(args) == 0 { if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name) return "", fmt.Errorf("missing time column argument for macro %v", name)
......
...@@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) { ...@@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')") sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)") So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/300)::bigint*300")
}) })
Convey("interpolate __timeTo function", func() { Convey("interpolate __timeTo function", func() {
......
...@@ -36,6 +36,8 @@ import 'brace/mode/text'; ...@@ -36,6 +36,8 @@ import 'brace/mode/text';
import 'brace/snippets/text'; import 'brace/snippets/text';
import 'brace/mode/sql'; import 'brace/mode/sql';
import 'brace/snippets/sql'; import 'brace/snippets/sql';
import 'brace/mode/markdown';
import 'brace/snippets/markdown';
const DEFAULT_THEME_DARK = "ace/theme/grafana-dark"; const DEFAULT_THEME_DARK = "ace/theme/grafana-dark";
const DEFAULT_THEME_LIGHT = "ace/theme/textmate"; const DEFAULT_THEME_LIGHT = "ace/theme/textmate";
......
...@@ -21,7 +21,7 @@ export class Timer { ...@@ -21,7 +21,7 @@ export class Timer {
} }
cancelAll() { cancelAll() {
_.each(this.timers, function (t) { _.each(this.timers, t => {
this.$timeout.cancel(t); this.$timeout.cancel(t);
}); });
this.timers = []; this.timers = [];
......
...@@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, ...@@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
text = text + '<br />' + event.text; text = text + '<br />' + event.text;
} }
} else if (title) { } else if (title) {
text = title + '<br />' + text; text = title + '<br />' + (_.isString(text) ? text : '');
title = ''; title = '';
} }
......
...@@ -49,7 +49,10 @@ export class SaveDashboardAsModalCtrl { ...@@ -49,7 +49,10 @@ export class SaveDashboardAsModalCtrl {
if (dashboard.id > 0) { if (dashboard.id > 0) {
this.clone.rows.forEach(row => { this.clone.rows.forEach(row => {
row.panels.forEach(panel => { row.panels.forEach(panel => {
delete panel.thresholds; if (panel.type === "graph" && panel.alert) {
delete panel.thresholds;
}
delete panel.alert; delete panel.alert;
}); });
}); });
......
import {SaveDashboardAsModalCtrl} from '../save_as_modal';
import {describe, it, expect} from 'test/lib/common';
describe('saving dashboard as', () => {
function scenario(name, panel, verify) {
describe(name, () => {
var json = {
title: "name",
rows: [ { panels: [
panel
]}]
};
var mockDashboardSrv = {
getCurrent: function() {
return {
id: 5,
getSaveModelClone: function() {
return json;
}
};
}
};
var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv);
var ctx: any = {
clone: ctrl.clone,
ctrl: ctrl,
panel: {}
};
for (let row of ctrl.clone.rows) {
for (let panel of row.panels) {
ctx.panel = panel;
}
}
it("verify", () => {
verify(ctx);
});
});
}
scenario("default values", {}, (ctx) => {
var clone = ctx.clone;
expect(clone.id).toBe(null);
expect(clone.title).toBe("name Copy");
expect(clone.editable).toBe(true);
expect(clone.hideControls).toBe(false);
});
var graphPanel = { id: 1, type: "graph", alert: { rule: 1}, thresholds: { value: 3000} };
scenario("should remove alert from graph panel", graphPanel , (ctx) => {
expect(ctx.panel.alert).toBe(undefined);
});
scenario("should remove threshold from graph panel", graphPanel, (ctx) => {
expect(ctx.panel.thresholds).toBe(undefined);
});
scenario("singlestat should keep threshold", { id: 1, type: "singlestat", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
scenario("table should keep threshold", { id: 1, type: "table", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
});
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time_sec</b> for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) - column with alias: <b>time_sec</b> for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column)
- column with alias <b>title</b> for the annotation title
- column with alias: <b>text</b> for the annotation text - column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' - column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'
......
...@@ -49,7 +49,15 @@ Macros: ...@@ -49,7 +49,15 @@ Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec - $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &ge; 1492750877 AND UNIX_TIMESTAMP(time_date_time) &le; 1492750877 - $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &ge; 1492750877 AND UNIX_TIMESTAMP(time_date_time) &le; 1492750877
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877 - $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
- $__timeGroup(column,'5m') -&gt; (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int - $__timeGroup(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
Example of group by and order by with $__timeGroup:
SELECT
$__timeGroup(timestamp_col, '1h') AS time,
sum(value_double) as value
FROM yourtable
GROUP BY 1
ORDER BY 1
Or build your own conditionals using these macros which just return the values: Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877) - $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)
......
...@@ -441,7 +441,7 @@ function (angular, _, dateMath) { ...@@ -441,7 +441,7 @@ function (angular, _, dateMath) {
} }
function mapMetricsToTargets(metrics, options, tsdbVersion) { function mapMetricsToTargets(metrics, options, tsdbVersion) {
var interpolatedTagValue; var interpolatedTagValue, arrTagV;
return _.map(metrics, function(metricData) { return _.map(metrics, function(metricData) {
if (tsdbVersion === 3) { if (tsdbVersion === 3) {
return metricData.query.index; return metricData.query.index;
...@@ -453,7 +453,8 @@ function (angular, _, dateMath) { ...@@ -453,7 +453,8 @@ function (angular, _, dateMath) {
return target.metric === metricData.metric && return target.metric === metricData.metric &&
_.every(target.tags, function(tagV, tagK) { _.every(target.tags, function(tagV, tagK) {
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars, 'pipe'); interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars, 'pipe');
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*"; arrTagV = interpolatedTagValue.split('|');
return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === "*";
}); });
} }
}); });
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time" - column with alias: <b>time</b> for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time"
- column with alias <b>title</b> for the annotation title
- column with alias: <b>text</b> for the annotation text - column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' - column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'
......
...@@ -50,17 +50,15 @@ Macros: ...@@ -50,17 +50,15 @@ Macros:
- $__timeEpoch -&gt; extract(epoch from column) as "time" - $__timeEpoch -&gt; extract(epoch from column) as "time"
- $__timeFilter(column) -&gt; column &ge; to_timestamp(1492750877) AND column &le; to_timestamp(1492750877) - $__timeFilter(column) -&gt; column &ge; to_timestamp(1492750877) AND column &le; to_timestamp(1492750877)
- $__unixEpochFilter(column) -&gt; column &gt; 1492750877 AND column &lt; 1492750877 - $__unixEpochFilter(column) -&gt; column &gt; 1492750877 AND column &lt; 1492750877
- $__timeGroup(column,'5m') -&gt; (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int
To group by time use $__timeGroup:
-&gt; (extract(epoch from column)/extract(epoch from column::interval))::int
Example of group by and order by with $__timeGroup: Example of group by and order by with $__timeGroup:
SELECT SELECT
min(date_time_col) AS time_sec, $__timeGroup(date_time_col, '1h') AS time,
sum(value_double) as value sum(value) as value
FROM yourtable FROM yourtable
group by $__timeGroup(date_time_col, '1h') GROUP BY time
order by $__timeGroup(date_time_col, '1h') ASC ORDER BY time
Or build your own conditionals using these macros which just return the values: Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; to_timestamp(1492750877) - $__timeFrom() -&gt; to_timestamp(1492750877)
......
<div class="panel-alert-list" style="{{ctrl.contentHeight}}"> <div class="panel-alert-list" style="{{ctrl.contentHeight}}">
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'"> <section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
<ol class="card-list"> <ol class="card-list">
<li class="card-item-wrapper" ng-show="!ctrl.currentAlerts.length">
<div class="alert-list card-item card-item--alert">
<div class="alert-list-body">
<div class="alert-list-main">
<p class="alert-list-title">
No alerts in selected interval
</p>
</div>
</div>
</div>
</li>
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts"> <li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
<div class="alert-list card-item card-item--alert"> <div class="alert-list card-item card-item--alert">
<div class="alert-list-body"> <div class="alert-list-body">
......
...@@ -15,5 +15,9 @@ ...@@ -15,5 +15,9 @@
(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported) (This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)
</span> </span>
<textarea class="gf-form-input" ng-model="ctrl.panel.content" rows="20" style="width:95%" give-focus="true" ng-change="ctrl.render()" ng-model-onblur> <div class="gf-form-inline">
</textarea> <div class="gf-form gf-form--grow">
<code-editor content="ctrl.panel.content" rows="20" on-change="ctrl.render()" data-mode="markdown" code-editor-focus="true">
</code-editor>
</div>
</div>
...@@ -23,6 +23,11 @@ export class TextPanelCtrl extends PanelCtrl { ...@@ -23,6 +23,11 @@ export class TextPanelCtrl extends PanelCtrl {
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.events.on('refresh', this.onRefresh.bind(this)); this.events.on('refresh', this.onRefresh.bind(this));
this.events.on('render', this.onRender.bind(this)); this.events.on('render', this.onRender.bind(this));
$scope.$watch('ctrl.panel.content',
_.throttle(() => {
this.render();
}, 1000)
);
} }
onInitEditMode() { onInitEditMode() {
...@@ -66,7 +71,9 @@ export class TextPanelCtrl extends PanelCtrl { ...@@ -66,7 +71,9 @@ export class TextPanelCtrl extends PanelCtrl {
}); });
} }
this.updateContent(this.remarkable.render(content)); this.$scope.$applyAsync(() => {
this.updateContent(this.remarkable.render(content));
});
} }
updateContent(html) { updateContent(html) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment