diff --git a/CHANGELOG.md b/CHANGELOG.md index c0833f60e37..751fe2d3feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/sources/index.md b/docs/sources/index.md index 9226c842abc..7a431e29692 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -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" >}}) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 4b155ae3208..4d7de98f2ea 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -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, }) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 36c38804a01..108b81fc5f3 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -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) diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index c92020d0aae..988612fb287 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -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) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 21400b03dfd..95932ab1c83 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -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) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index ba991e6f2d5..ff268805259 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -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() { diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 2615a635c7e..cc3b1e46ad4 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -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"; diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 6356e1f2910..6355105ee0e 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -21,7 +21,7 @@ export class Timer { } cancelAll() { - _.each(this.timers, function (t) { + _.each(this.timers, t => { this.$timeout.cancel(t); }); this.timers = []; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index c950d3edd55..4828eb671a6 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, text = text + '
' + event.text; } } else if (title) { - text = title + '
' + text; + text = title + '
' + (_.isString(text) ? text : ''); title = ''; } diff --git a/public/app/features/dashboard/save_as_modal.ts b/public/app/features/dashboard/save_as_modal.ts index 7e0b754d559..99bf21ca600 100644 --- a/public/app/features/dashboard/save_as_modal.ts +++ b/public/app/features/dashboard/save_as_modal.ts @@ -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; }); }); diff --git a/public/app/features/dashboard/specs/save_as_modal.jest.ts b/public/app/features/dashboard/specs/save_as_modal.jest.ts new file mode 100644 index 00000000000..913209eb01f --- /dev/null +++ b/public/app/features/dashboard/specs/save_as_modal.jest.ts @@ -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); + }); +}); diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index 09581e2a552..b34eff5b011 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -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: time_sec for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) -- column with alias title for the annotation title - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index a7e993afd7f..22d64c9190f 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -49,7 +49,15 @@ Macros: - $__time(column) -> UNIX_TIMESTAMP(column) as time_sec - $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) ≥ 1492750877 AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877 - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 -- $__timeGroup(column,'5m') -> (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int +- $__timeGroup(column,'5m') -> 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() -> FROM_UNIXTIME(1492750877) diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 4d51b117ed4..7315485c6db 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -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 === "*"; }); } }); diff --git a/public/app/plugins/datasource/postgres/partials/annotations.editor.html b/public/app/plugins/datasource/postgres/partials/annotations.editor.html index 07b838e739a..b56f7523087 100644 --- a/public/app/plugins/datasource/postgres/partials/annotations.editor.html +++ b/public/app/plugins/datasource/postgres/partials/annotations.editor.html @@ -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: time for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time" -- column with alias title for the annotation title - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 1939fc47ecb..f1c7b376353 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -50,17 +50,15 @@ Macros: - $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column ≥ to_timestamp(1492750877) AND column ≤ to_timestamp(1492750877) - $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 - -To group by time use $__timeGroup: --> (extract(epoch from column)/extract(epoch from column::interval))::int +- $__timeGroup(column,'5m') -> (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() -> to_timestamp(1492750877) diff --git a/public/app/plugins/panel/alertlist/module.html b/public/app/plugins/panel/alertlist/module.html index a88c4ebadc7..0a3ff4fabb4 100644 --- a/public/app/plugins/panel/alertlist/module.html +++ b/public/app/plugins/panel/alertlist/module.html @@ -1,6 +1,17 @@
    +
  1. +
    +
    +
    +

    + No alerts in selected interval +

    +
    +
    +
    +
  2. diff --git a/public/app/plugins/panel/text/editor.html b/public/app/plugins/panel/text/editor.html index 8e1283a39ba..eab53dc7615 100644 --- a/public/app/plugins/panel/text/editor.html +++ b/public/app/plugins/panel/text/editor.html @@ -15,5 +15,9 @@ (This area uses Markdown. HTML is not supported) - +
    +
    + + +
    +
    diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 5f453aea15b..a3a58e968fc 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -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) {