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

This commit is contained in:
Torkel Ödegaard
2017-10-31 09:01:10 +01:00
20 changed files with 156 additions and 25 deletions

View File

@@ -11,7 +11,11 @@
## 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)
* **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
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
@@ -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)
* **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)
## Fixes

View File

@@ -46,8 +46,8 @@ those options.
- [Graphite]({{< relref "features/datasources/graphite.md" >}})
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
- [Prometheus]({{< relref "features/datasources/influxdb.md" >}})
- [OpenTSDB]({{< relref "features/datasources/prometheus.md" >}})
- [Prometheus]({{< relref "features/datasources/prometheus.md" >}})
- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
- [MySQL]({{< relref "features/datasources/mysql.md" >}})
- [Postgres]({{< relref "features/datasources/postgres.md" >}})
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})

View File

@@ -225,7 +225,7 @@ func init() {
M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{
Name: "api_dataproxy_request_all_milliseconds",
Help: "summary for dashboard search duration",
Help: "summary for dataproxy request duration",
Namespace: exporterName,
})

View File

@@ -3,6 +3,8 @@ package mysql
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -25,7 +27,7 @@ func (m *MySqlMacroEngine) Interpolate(timeRange *tsdb.TimeRange, sql string) (s
var macroError error
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 {
macroError = err
return "macro_error()"
@@ -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
case "__timeTo":
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":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@@ -40,6 +40,14 @@ func TestMacroEngine(t *testing.T) {
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() {
sql, err := engine.Interpolate(timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -80,10 +81,14 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
case "__timeTo":
return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeGroup":
if len(args) < 2 {
if len(args) != 2 {
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":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
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() {

View File

@@ -36,6 +36,8 @@ import 'brace/mode/text';
import 'brace/snippets/text';
import 'brace/mode/sql';
import 'brace/snippets/sql';
import 'brace/mode/markdown';
import 'brace/snippets/markdown';
const DEFAULT_THEME_DARK = "ace/theme/grafana-dark";
const DEFAULT_THEME_LIGHT = "ace/theme/textmate";

View File

@@ -21,7 +21,7 @@ export class Timer {
}
cancelAll() {
_.each(this.timers, function (t) {
_.each(this.timers, t => {
this.$timeout.cancel(t);
});
this.timers = [];

View File

@@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
text = text + '<br />' + event.text;
}
} else if (title) {
text = title + '<br />' + text;
text = title + '<br />' + (_.isString(text) ? text : '');
title = '';
}

View File

@@ -49,7 +49,10 @@ export class SaveDashboardAsModalCtrl {
if (dashboard.id > 0) {
this.clone.rows.forEach(row => {
row.panels.forEach(panel => {
delete panel.thresholds;
if (panel.type === "graph" && panel.alert) {
delete panel.thresholds;
}
delete panel.alert;
});
});

View File

@@ -0,0 +1,67 @@
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);
});
});

View File

@@ -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.
- 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>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'

View File

@@ -49,7 +49,15 @@ Macros:
- $__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
- $__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:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)

View File

@@ -441,7 +441,7 @@ function (angular, _, dateMath) {
}
function mapMetricsToTargets(metrics, options, tsdbVersion) {
var interpolatedTagValue;
var interpolatedTagValue, arrTagV;
return _.map(metrics, function(metricData) {
if (tsdbVersion === 3) {
return metricData.query.index;
@@ -453,7 +453,8 @@ function (angular, _, dateMath) {
return target.metric === metricData.metric &&
_.every(target.tags, function(tagV, tagK) {
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars, 'pipe');
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
arrTagV = interpolatedTagValue.split('|');
return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === "*";
});
}
});

View File

@@ -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.
- 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>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'

View File

@@ -50,17 +50,15 @@ Macros:
- $__timeEpoch -&gt; extract(epoch from column) as "time"
- $__timeFilter(column) -&gt; column &ge; to_timestamp(1492750877) AND column &le; to_timestamp(1492750877)
- $__unixEpochFilter(column) -&gt; column &gt; 1492750877 AND column &lt; 1492750877
To group by time use $__timeGroup:
-&gt; (extract(epoch from column)/extract(epoch from column::interval))::int
- $__timeGroup(column,'5m') -&gt; (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int
Example of group by and order by with $__timeGroup:
SELECT
min(date_time_col) AS time_sec,
sum(value_double) as value
$__timeGroup(date_time_col, '1h') AS time,
sum(value) as value
FROM yourtable
group by $__timeGroup(date_time_col, '1h')
order by $__timeGroup(date_time_col, '1h') ASC
GROUP BY time
ORDER BY time
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; to_timestamp(1492750877)

View File

@@ -1,6 +1,17 @@
<div class="panel-alert-list" style="{{ctrl.contentHeight}}">
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
<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">
<div class="alert-list card-item card-item--alert">
<div class="alert-list-body">

View File

@@ -15,5 +15,9 @@
(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)
</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>
</textarea>
<div class="gf-form-inline">
<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>

View File

@@ -23,6 +23,11 @@ export class TextPanelCtrl extends PanelCtrl {
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.events.on('refresh', this.onRefresh.bind(this));
this.events.on('render', this.onRender.bind(this));
$scope.$watch('ctrl.panel.content',
_.throttle(() => {
this.render();
}, 1000)
);
}
onInitEditMode() {
@@ -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) {