From 7b5b94607b2956ab81d05c34fbb4c2e2fc615ab7 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 31 Jul 2018 12:51:07 +0200 Subject: [PATCH 01/23] fixed color for links in colored cells by adding a new variable that sets color: white when cell or row has background-color --- public/app/plugins/panel/table/renderer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index f6950dada52..456dadf6241 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -214,15 +214,20 @@ export class TableRenderer { var style = ''; var cellClasses = []; var cellClass = ''; + var linkStyle = ''; + + if (this.colorState.row) { + linkStyle = ' style="color: white"'; + } if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; + linkStyle = ' style="color: white;"'; this.colorState.cell = null; } else if (this.colorState.value) { style = ' style="color:' + this.colorState.value + '"'; this.colorState.value = null; } - // because of the fixed table headers css only solution // there is an issue if header cell is wider the cell // this hack adds header content to cell (not visible) @@ -253,7 +258,7 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; From 4b8ec4e32330b9fd606acc39604a8b9de7229ac3 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 31 Jul 2018 13:07:43 +0200 Subject: [PATCH 02/23] removed a blank space in div --- public/app/plugins/panel/table/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 456dadf6241..c1e4e6243f9 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -258,7 +258,7 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; From 43295f9c189e5fd5539892162562be7d33046603 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 30 Jul 2018 13:23:29 +0200 Subject: [PATCH 03/23] remove alias from postgres $__timeGroup macro --- docs/sources/features/datasources/postgres.md | 2 +- pkg/tsdb/postgres/macros.go | 2 +- pkg/tsdb/postgres/macros_test.go | 4 ++-- pkg/tsdb/postgres/postgres_test.go | 6 +++--- .../plugins/datasource/postgres/partials/query.editor.html | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 793b3b6f4c0..7915f29fcdc 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -60,7 +60,7 @@ Macro example | Description *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 661dbf3d4ce..852e9d7997e 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -109,7 +109,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, m.query.Model.Set("fillValue", floatVal) } } - return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%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 194573be0fd..bb947d4f01f 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -53,7 +53,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") }) Convey("interpolate __timeGroup function with spaces between args", func() { @@ -61,7 +61,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") }) Convey("interpolate __timeTo function", func() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index c7787929a9d..3e864dca1e6 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -183,7 +183,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -227,7 +227,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -281,7 +281,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index b7c12471f52..1ace05abae2 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -53,7 +53,7 @@ Macros: - $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time +- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 Example of group by and order by with $__timeGroup: SELECT From bd77541e092e022166a265d81e26583f6305de14 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 08:00:43 +0200 Subject: [PATCH 04/23] adjust test dashboards --- .../datasource_tests_postgres_unittest.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 2243baed0aa..a3139bf99f7 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -871,7 +871,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" } ], @@ -956,7 +956,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') AS time, \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], @@ -2352,5 +2352,5 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 1 -} \ No newline at end of file + "version": 17 +} From 42f189282618fb5ce42efc7f8cf804bdb1da65da Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 08:48:22 +0200 Subject: [PATCH 05/23] Add $__timeGroupAlias to postgres macros --- .../datasource_tests_postgres_unittest.json | 17 +++++++++-------- pkg/tsdb/postgres/macros.go | 6 ++++++ pkg/tsdb/postgres/macros_test.go | 14 ++++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index a3139bf99f7..3c2b34df78c 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -956,7 +956,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') AS time, \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], @@ -2352,5 +2352,6 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 17 + "version": 1 } + diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 852e9d7997e..fa887032c5d 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -110,6 +110,12 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, } } return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err 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 bb947d4f01f..ec74470a803 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -50,18 +50,24 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces between args", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column , '5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeTo function", func() { From d4d896ade829300fa306bac82798d746a85e9693 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 1 Aug 2018 09:08:17 +0200 Subject: [PATCH 06/23] replaced style with class for links --- public/app/plugins/panel/table/renderer.ts | 13 +++++++++---- .../app/plugins/panel/table/specs/renderer.jest.ts | 2 +- public/sass/components/_panel_table.scss | 4 ++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index c1e4e6243f9..474e9c89493 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -214,15 +214,15 @@ export class TableRenderer { var style = ''; var cellClasses = []; var cellClass = ''; - var linkStyle = ''; + var linkClass = ''; if (this.colorState.row) { - linkStyle = ' style="color: white"'; + linkClass = 'table-panel-link'; } if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; - linkStyle = ' style="color: white;"'; + linkClass = 'table-panel-link'; this.colorState.cell = null; } else if (this.colorState.value) { style = ' style="color:' + this.colorState.value + '"'; @@ -258,7 +258,12 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.jest.ts index 22957d1aa66..f1a686fb739 100644 --- a/public/app/plugins/panel/table/specs/renderer.jest.ts +++ b/public/app/plugins/panel/table/specs/renderer.jest.ts @@ -268,7 +268,7 @@ describe('when rendering table', () => { var expectedHtml = ` + target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right" class=""> host1 diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 8e0ecf15896..99e91f8ff67 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -133,3 +133,7 @@ height: 0px; line-height: 0px; } + +.table-panel-link { + color: white; +} From dc22e24642f79b1130de2fc4f15d911c2973f5d0 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 15:06:18 +0200 Subject: [PATCH 07/23] add compatibility code to handle pre 5.3 usage --- pkg/tsdb/postgres/macros.go | 17 +++++++++++++++++ pkg/tsdb/postgres/macros_test.go | 19 ++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index fa887032c5d..9e337caf3ec 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -30,6 +30,23 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim var macroError error sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + + // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility + // if there is a ',' directly after the macro call $__timeGroup is probably used + // in the old way. Inside window function ORDER BY $__timeGroup will be followed + // by ')' + if groups[1] == "__timeGroup" { + if index := strings.Index(sql, groups[0]); index >= 0 { + index += len(groups[0]) + if len(sql) > index { + // check for character after macro expression + if sql[index] == ',' { + groups[1] = "__timeGroupAlias" + } + } + } + } + args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index ec74470a803..beeea93893b 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -48,14 +48,27 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339))) }) + Convey("interpolate __timeGroup function pre 5.3 compatibility", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 AS \"time\", value") + + sql, err = engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m') as time, value") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 as time, value") + }) + Convey("interpolate __timeGroup function", func() { - sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')") + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')") + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroupAlias(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300") So(sql2, ShouldEqual, sql+" AS \"time\"") }) From bb7e5838635fa044e75507c03827e4ba97cb7f53 Mon Sep 17 00:00:00 2001 From: Brice Maron Date: Wed, 1 Aug 2018 19:38:13 +0200 Subject: [PATCH 08/23] fix custom variable quoting in sql* query interpolations --- public/app/plugins/datasource/mssql/datasource.ts | 4 ++-- .../app/plugins/datasource/mssql/specs/datasource.jest.ts | 7 +++++++ public/app/plugins/datasource/mysql/datasource.ts | 4 ++-- .../app/plugins/datasource/mysql/specs/datasource.jest.ts | 7 +++++++ public/app/plugins/datasource/postgres/datasource.ts | 4 ++-- .../plugins/datasource/postgres/specs/datasource.jest.ts | 7 +++++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 6656d4f96f7..dab7335ec97 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -16,7 +16,7 @@ export class MssqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -31,7 +31,7 @@ export class MssqlDatasource { return value; } - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts index dd2d4a60cec..0308717775b 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts @@ -218,6 +218,13 @@ describe('MSSQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 42fcf7b4564..67bb9d0a817 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -16,7 +16,7 @@ export class MysqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -31,7 +31,7 @@ export class MysqlDatasource { return value; } - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts index be33f5f8858..85fa2b8cc4e 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts @@ -214,6 +214,13 @@ describe('MySQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8eee389d1a5..644c9e48b9b 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -16,7 +16,7 @@ export class PostgresDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -27,7 +27,7 @@ export class PostgresDatasource { } var quotedValues = _.map(value, function(val) { - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts index 107cd76e6c5..cd6f57ee3fc 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts @@ -215,6 +215,13 @@ describe('PostgreSQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; From b71d10a7a42d9b47b191e981576cb17363f11a9d Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 20:58:51 +0200 Subject: [PATCH 09/23] add $__timeGroupAlias to mysql and mssql --- pkg/tsdb/mssql/macros.go | 6 ++++++ pkg/tsdb/mssql/macros_test.go | 6 ++++++ pkg/tsdb/mysql/macros.go | 6 ++++++ pkg/tsdb/mysql/macros_test.go | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 2c16b5cb27f..f33ab1d40be 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -110,6 +110,12 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er } } return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS [time]", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index 1895cd99442..ea50c418de7 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -55,15 +55,21 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with fill (value = NULL)", func() { diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 078d1ff54f8..a56fd1ceb2a 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -105,6 +105,12 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er } } return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err 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 003af9a737f..fd9d3f5688a 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -38,16 +38,22 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeFilter function", func() { From 82c473e3af4800a8cb9f20c96530190b6c44d847 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 21:23:00 +0200 Subject: [PATCH 10/23] document $__timeGroupAlias --- docs/sources/features/datasources/mssql.md | 1 + docs/sources/features/datasources/mysql.md | 1 + docs/sources/features/datasources/postgres.md | 1 + public/app/plugins/datasource/mssql/partials/query.editor.html | 1 + public/app/plugins/datasource/mysql/partials/query.editor.html | 1 + .../app/plugins/datasource/postgres/partials/query.editor.html | 1 + 6 files changed, 6 insertions(+) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index ea7be8e1c30..dabb896ec0f 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -82,6 +82,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 22287b2a838..a0e67037005 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -65,6 +65,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* *$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 7915f29fcdc..35dfcac15c0 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -62,6 +62,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index 397a35164c0..e1320aabde2 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeGroupAlias(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] Example of group by and order by with $__timeGroup: SELECT diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index d4be22fc3e9..db12a3fe8ce 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 - $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) +- $__timeGroupAlias(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" Example of group by and order by with $__timeGroup: SELECT diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 1ace05abae2..1b7278f6809 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 +- $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time" Example of group by and order by with $__timeGroup: SELECT From 36d981597ed1e6b22ada8c49884f99b7d02444d0 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 2 Aug 2018 11:18:21 +0200 Subject: [PATCH 11/23] removed table-panel-link class and add a class white to modify table-panel-cell-link class --- public/app/plugins/panel/table/renderer.ts | 18 ++++++------------ .../plugins/panel/table/specs/renderer.jest.ts | 2 +- public/sass/components/_panel_table.scss | 6 ++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 474e9c89493..e4d3626c3b9 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -214,15 +214,10 @@ export class TableRenderer { var style = ''; var cellClasses = []; var cellClass = ''; - var linkClass = ''; - - if (this.colorState.row) { - linkClass = 'table-panel-link'; - } if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; - linkClass = 'table-panel-link'; + cellClasses.push('white'); this.colorState.cell = null; } else if (this.colorState.value) { style = ' style="color:' + this.colorState.value + '"'; @@ -257,13 +252,12 @@ export class TableRenderer { var cellTarget = column.style.linkTargetBlank ? '_blank' : ''; cellClasses.push('table-panel-cell-link'); + + if (this.colorState.row) { + cellClasses.push('white'); + } columnHtml += ` - + ${value} `; diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.jest.ts index f1a686fb739..22957d1aa66 100644 --- a/public/app/plugins/panel/table/specs/renderer.jest.ts +++ b/public/app/plugins/panel/table/specs/renderer.jest.ts @@ -268,7 +268,7 @@ describe('when rendering table', () => { var expectedHtml = ` + target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right"> host1 diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 99e91f8ff67..c793cd408b6 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -87,6 +87,12 @@ height: 100%; display: inline-block; } + + &.white { + a { + color: white; + } + } } &.cell-highlighted:hover { From b03e3242e3ee4092ad7cc81219d35a39a2cd6c40 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 2 Aug 2018 11:21:17 +0200 Subject: [PATCH 12/23] removed table-panel-link class --- public/sass/components/_panel_table.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index c793cd408b6..fc14236c2b7 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -139,7 +139,3 @@ height: 0px; line-height: 0px; } - -.table-panel-link { - color: white; -} From a8976f6c36005fcbec485f001766560b0767f45f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 11:43:48 +0200 Subject: [PATCH 13/23] changelog: add notes about closing #12785 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa089b5900b..66ab1906c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) From 04fcd2a05481c799176420e802bd73ec24d699a0 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 2 Aug 2018 18:49:40 +0900 Subject: [PATCH 14/23] add series override option to hide tooltip (#12378) * add series override option to hide tooltip * fix test * invert option * fix test * remove initialization --- public/app/core/time_series2.ts | 4 ++++ public/app/plugins/panel/graph/graph_tooltip.ts | 5 +++++ .../app/plugins/panel/graph/series_overrides_ctrl.ts | 1 + .../plugins/panel/graph/specs/graph_tooltip.jest.ts | 11 ++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 59729ebc312..f4d0943d52f 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -76,6 +76,7 @@ export default class TimeSeries { valueFormater: any; stats: any; legend: boolean; + hideTooltip: boolean; allIsNull: boolean; allIsZero: boolean; decimals: number; @@ -181,6 +182,9 @@ export default class TimeSeries { if (override.legend !== void 0) { this.legend = override.legend; } + if (override.hideTooltip !== void 0) { + this.hideTooltip = override.hideTooltip; + } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 509d15b8a25..7bbafc453eb 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -81,6 +81,11 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { continue; } + if (series.hideTooltip) { + results[0].push({ hidden: true, value: 0 }); + continue; + } + hoverIndex = this.findHoverIndexFromData(pos.x, series); hoverDistance = pos.x - series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0]; diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index 5958c80bac9..024c9cac93b 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -152,6 +152,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); $scope.addOverrideOption('Legend', 'legend', [true, false]); + $scope.addOverrideOption('Hide in tooltip', 'hideTooltip', [true, false]); $scope.updateCurrentOverrides(); } diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts index 3bc60ed8ea3..baebf2c5930 100644 --- a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts +++ b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts @@ -68,7 +68,10 @@ describe('findHoverIndexFromData', function() { describeSharedTooltip('steppedLine false, stack false', function(ctx) { ctx.setup(function() { - ctx.data = [{ data: [[10, 15], [12, 20]], lines: {} }, { data: [[10, 2], [12, 3]], lines: {} }]; + ctx.data = [ + { data: [[10, 15], [12, 20]], lines: {}, hideTooltip: false }, + { data: [[10, 2], [12, 3]], lines: {}, hideTooltip: false }, + ]; ctx.pos = { x: 11 }; }); @@ -105,6 +108,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -114,6 +118,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio points: [[10, 2], [12, 3]], }, stack: true, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; @@ -136,6 +141,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -145,6 +151,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s points: [[10, 2], [12, 3]], }, stack: false, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; @@ -167,6 +174,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -176,6 +184,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function points: [[10, 2], [12, 3]], }, stack: false, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; From 169fcba52031b104a39b1442edf655e9372541f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 2 Aug 2018 11:51:41 +0200 Subject: [PATCH 15/23] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ab1906c4d..22c24f83b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) +* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/pull/3341), thx [@mtanda](https://github.com/mtanda) + # 5.2.2 (2018-07-25) From 57910549b6b8eb639c6cd36814d3a0850a123bf2 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 2 Aug 2018 12:37:50 +0200 Subject: [PATCH 16/23] Improve iOS and Windows 10 experience (#12769) * Improve iOS homescreen icon * Improve Windows10 tile experience * Remove unused favicon --- public/img/apple-touch-icon.png | Bin 0 -> 15718 bytes public/img/browserconfig.xml | 9 +++++++++ public/img/mstile-150x150.png | Bin 0 -> 9010 bytes public/views/index.template.html | 7 +++++-- 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 public/img/apple-touch-icon.png create mode 100644 public/img/browserconfig.xml create mode 100644 public/img/mstile-150x150.png diff --git a/public/img/apple-touch-icon.png b/public/img/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3031d9aa011fb298c06563370d6b669c0d0232c2 GIT binary patch literal 15718 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 + + + + + #2b5797 + + + diff --git a/public/img/mstile-150x150.png b/public/img/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..2360303f2ad55e968b56f19dd18ab2d06f542120 GIT binary patch literal 9010 zcmdtIS2$eX7dJk7q6Uc)gGBE}@1!6^5WTlxL^pb8A|VMzg6Jg)L86V`8IcTP@YSL- z$%Ih`6B7nAc;|Qdzxdz27wtc-P{Mf&SZFvy@v}A4kNfh0>kryx9#t zadBJsGR8K}@1}0u(3i(^T)R8f8vD2V!~2&Xn^jzzRZSgx#0fn~B{Df)VoEtElyS}h z=}0pWJW=RVMi3zshn3yX|G!j3T)Cp?y?*-#+S{frdUpmRP9w*6sv0^8B7ZH>Z!(r( z&R_xDJH)U8j5d-+?`zlXUwuh6u6=8miHdLcq4%ekPR`Dn^@U1c5E^*=!oE+4t?24N zM|8x@ekVcdQ#InYaI5mHxf0U>wJjAv3D{1DZ80Vmb2SDnaYdDVYQ3)R_44P9hzr`%?F z;a|uKR_J@RG^+>Z?a>T#Pm#x6no!o8fQZzD$_Rt=XPuJ|UYkSj8$AzUMlNE^M${RZ ze7+~>TE;U1t(*z3z26~vZhx8Q!S?y{hFFZf_CA7?=U>mK9=^_vT1)&c8Z%zcG8X1L@vhI>VTJb%58b*TZqC5y zbORCR#Fux;xPXps;dSy){Fw_hc~bvCwiu?w7C;5!_PogoYitY*h*d>$lr+x*wO+KT z$_y}Ln66_FZe7Jut5WTT2rwkk;)tDk-h&--XN%n#+)A=6W3|k#uTT9EuJkw>YpSs1 z&|7FS(7ep0C3dx{b1d|W1-jr~k(*Qgm5mPq(JBj!85K}(QPe$-E!qoevOL~uMX=Tv z2&`IPg2(gXxiV&w@4uur5bFt%tMPS?i~CIBV1+KAcD%WRP3HGl_{=j5m=r$yDR4WB z)h%Z9%{~{}T%D+)?S=F}Y#^X_goY~*{tD>T9%D_lUc0_gyaV;*@DH-Ly#ZHyQo_{B zSo2cLoLA&kkniT3Pajn?RZBemZ_ex9BpN>w{Q@j~?r1Ah>;o2m;>_Gj%|igRa=1Wg z-GA}qIMViVJVs00ri1aQ4D>gw+^hL!|8zmf9eV%8V8BQ4xQ_aA^7tLC&2jbO*u}m; zW6!adlVOxGvYo2p?o8DSgV1IqelPl6i`W*p!1?

9TcXe4aBrtVUSe8<)-#McI2T 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