Commit 8838e430 by Torkel Ödegaard

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

parents 9d5e4bee 7583b78c
......@@ -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
......
......@@ -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" >}})
......@@ -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,
})
......
......@@ -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)
......
......@@ -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)
......
......@@ -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)
......
......@@ -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() {
......
......@@ -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";
......
......@@ -21,7 +21,7 @@ export class Timer {
}
cancelAll() {
_.each(this.timers, function (t) {
_.each(this.timers, t => {
this.$timeout.cancel(t);
});
this.timers = [];
......
......@@ -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 = '';
}
......
......@@ -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;
});
});
......
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 @@
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'
......
......@@ -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)
......
......@@ -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 === "*";
});
}
});
......
......@@ -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'
......
......@@ -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)
......
<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">
......
......@@ -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>
......@@ -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) {
......
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