From 68c460a957356b606960e18b77f22b6efdde72a6 Mon Sep 17 00:00:00 2001 From: Yuan Liu Date: Fri, 19 Oct 2018 17:17:38 +0800 Subject: [PATCH 01/33] fix cannot receive dingding alert bug --- pkg/services/alerting/notifiers/dingding.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 738e43af2d2..1ef085c82f1 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -57,6 +57,9 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message := evalContext.Rule.Message picUrl := evalContext.ImagePublicUrl title := evalContext.GetNotificationTitle() + if message == "" { + message = title + } bodyJSON, err := simplejson.NewJson([]byte(`{ "msgtype": "link", From 374fe9dcb4e3eb2bbf9818e68cd9300da3da2b87 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 19 Oct 2018 15:29:58 +0200 Subject: [PATCH 02/33] Explore: reuse table merge from table panel - Extracted table panel's merge logic to combine multiple tables into one - Put the merge logic into the table model as it merges multiple table models - make use of merge in Explore's table query response handler - copied tests over to table model spec, kept essential tests in transformer spec --- public/app/core/specs/table_model.test.ts | 117 +++++++++++++++++- public/app/core/table_model.ts | 110 +++++++++++++++- public/app/features/explore/Explore.tsx | 7 +- public/app/features/explore/Table.tsx | 3 + .../panel/table/specs/transformers.test.ts | 48 ------- .../app/plugins/panel/table/transformers.ts | 98 +-------------- 6 files changed, 237 insertions(+), 146 deletions(-) diff --git a/public/app/core/specs/table_model.test.ts b/public/app/core/specs/table_model.test.ts index 990daaaa2da..19b2a7543fb 100644 --- a/public/app/core/specs/table_model.test.ts +++ b/public/app/core/specs/table_model.test.ts @@ -1,4 +1,4 @@ -import TableModel from 'app/core/table_model'; +import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; describe('when sorting table desc', () => { let table; @@ -79,3 +79,118 @@ describe('when sorting with nulls', () => { expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']); }); }); + +describe('mergeTables', () => { + const time = new Date().getTime(); + + const singleTable = new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }], + rows: [[time, 'Label Value 1', 42]], + }); + + const multipleTablesSameColumns = [ + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #A' }], + rows: [[time, 'Label Value 1', 'Label Value 2', 42]], + }), + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #B' }], + rows: [[time, 'Label Value 1', 'Label Value 2', 13]], + }), + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }], + rows: [[time, 'Label Value 1', 'Label Value 2', 4]], + }), + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }], + rows: [[time, 'Label Value 1', 'Label Value 2', 7]], + }), + ]; + + const multipleTablesDifferentColumns = [ + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }], + rows: [[time, 'Label Value 1', 42]], + }), + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 2' }, { text: 'Value #B' }], + rows: [[time, 'Label Value 2', 13]], + }), + new TableModel({ + type: 'table', + columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #C' }], + rows: [[time, 'Label Value 3', 7]], + }), + ]; + + it('should return the single table as is', () => { + const table = mergeTablesIntoModel(new TableModel(), singleTable); + expect(table.columns.length).toBe(3); + expect(table.columns[0].text).toBe('Time'); + expect(table.columns[1].text).toBe('Label Key 1'); + expect(table.columns[2].text).toBe('Value'); + }); + + it('should return the union of columns for multiple tables', () => { + const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns); + expect(table.columns.length).toBe(6); + expect(table.columns[0].text).toBe('Time'); + expect(table.columns[1].text).toBe('Label Key 1'); + expect(table.columns[2].text).toBe('Label Key 2'); + expect(table.columns[3].text).toBe('Value #A'); + expect(table.columns[4].text).toBe('Value #B'); + expect(table.columns[5].text).toBe('Value #C'); + }); + + it('should return 1 row for a single table', () => { + const table = mergeTablesIntoModel(new TableModel(), singleTable); + expect(table.rows.length).toBe(1); + expect(table.rows[0][0]).toBe(time); + expect(table.rows[0][1]).toBe('Label Value 1'); + expect(table.rows[0][2]).toBe(42); + }); + + it('should return 2 rows for a multiple tables with same column values plus one extra row', () => { + const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns); + expect(table.rows.length).toBe(2); + expect(table.rows[0][0]).toBe(time); + expect(table.rows[0][1]).toBe('Label Value 1'); + expect(table.rows[0][2]).toBe('Label Value 2'); + expect(table.rows[0][3]).toBe(42); + expect(table.rows[0][4]).toBe(13); + expect(table.rows[0][5]).toBe(4); + expect(table.rows[1][0]).toBe(time); + expect(table.rows[1][1]).toBe('Label Value 1'); + expect(table.rows[1][2]).toBe('Label Value 2'); + expect(table.rows[1][3]).toBeUndefined(); + expect(table.rows[1][4]).toBeUndefined(); + expect(table.rows[1][5]).toBe(7); + }); + + it('should return 2 rows for multiple tables with different column values', () => { + const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesDifferentColumns); + expect(table.rows.length).toBe(2); + expect(table.columns.length).toBe(6); + + expect(table.rows[0][0]).toBe(time); + expect(table.rows[0][1]).toBe('Label Value 1'); + expect(table.rows[0][2]).toBe(42); + expect(table.rows[0][3]).toBe('Label Value 2'); + expect(table.rows[0][4]).toBe(13); + expect(table.rows[0][5]).toBeUndefined(); + + expect(table.rows[1][0]).toBe(time); + expect(table.rows[1][1]).toBe('Label Value 3'); + expect(table.rows[1][2]).toBeUndefined(); + expect(table.rows[1][3]).toBeUndefined(); + expect(table.rows[1][4]).toBeUndefined(); + expect(table.rows[1][5]).toBe(7); + }); +}); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index f8b96d0537b..99395258ba3 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,3 +1,5 @@ +import _ from 'lodash'; + interface Column { text: string; title?: string; @@ -14,11 +16,20 @@ export default class TableModel { type: string; columnMap: any; - constructor() { + constructor(table?: any) { this.columns = []; this.columnMap = {}; this.rows = []; this.type = 'table'; + + if (table) { + if (table.columns) { + table.columns.forEach(col => this.addColumn(col)); + } + if (table.rows) { + table.rows.forEach(row => this.addRow(row)); + } + } } sort(options) { @@ -52,3 +63,100 @@ export default class TableModel { this.rows.push(row); } } + +// Returns true if both rows have matching non-empty fields as well as matching +// indexes where one field is empty and the other is not +function areRowsMatching(columns, row, otherRow) { + let foundFieldToMatch = false; + for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { + if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) { + if (row[columnIndex] !== otherRow[columnIndex]) { + return false; + } + } else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) { + foundFieldToMatch = true; + } + } + return foundFieldToMatch; +} + +export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel { + const model = dst || new TableModel(); + + // Single query returns data columns and rows as is + if (arguments.length === 2) { + model.columns = [...tables[0].columns]; + model.rows = [...tables[0].rows]; + return model; + } + + // Track column indexes of union: name -> index + const columnNames = {}; + + // Union of all non-value columns + const columnsUnion = tables.slice().reduce((acc, series) => { + series.columns.forEach(col => { + const { text } = col; + if (columnNames[text] === undefined) { + columnNames[text] = acc.length; + acc.push(col); + } + }); + return acc; + }, []); + + // Map old column index to union index per series, e.g., + // given columnNames {A: 0, B: 1} and + // data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]] + const columnIndexMapper = tables.map(series => series.columns.map(col => columnNames[col.text])); + + // Flatten rows of all series and adjust new column indexes + const flattenedRows = tables.reduce((acc, series, seriesIndex) => { + const mapper = columnIndexMapper[seriesIndex]; + series.rows.forEach(row => { + const alteredRow = []; + // Shifting entries according to index mapper + mapper.forEach((to, from) => { + alteredRow[to] = row[from]; + }); + acc.push(alteredRow); + }); + return acc; + }, []); + + // Merge rows that have same values for columns + const mergedRows = {}; + const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => { + if (!mergedRows[rowIndex]) { + // Look from current row onwards + let offset = rowIndex + 1; + // More than one row can be merged into current row + while (offset < flattenedRows.length) { + // Find next row that could be merged + const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset); + if (match > -1) { + const matchedRow = flattenedRows[match]; + // Merge values from match into current row if there is a gap in the current row + for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) { + if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) { + row[columnIndex] = matchedRow[columnIndex]; + } + } + // Don't visit this row again + mergedRows[match] = matchedRow; + // Keep looking for more rows to merge + offset = match + 1; + } else { + // No match found, stop looking + break; + } + } + acc.push(row); + } + return acc; + }, []); + + model.columns = columnsUnion; + model.rows = compactedRows; + return model; +} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 4fe67d9d37b..d7326a5bfd1 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -13,6 +13,7 @@ import ResetStyles from 'app/core/components/Picker/ResetStyles'; import PickerOption from 'app/core/components/Picker/PickerOption'; import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'; import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; +import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import ElapsedTime from './ElapsedTime'; import QueryRows from './QueryRows'; @@ -389,8 +390,10 @@ export class Explore extends React.PureComponent { to: parseDate(range.to, true), }; const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval); - const targets = this.queryExpressions.map(q => ({ + const targets = this.queryExpressions.map((q, i) => ({ ...targetOptions, + // Target identifier is needed for table transformations + refId: i + 1, expr: q, })); return { @@ -437,7 +440,7 @@ export class Explore extends React.PureComponent { }); try { const res = await datasource.query(options); - const tableModel = res.data[0]; + const tableModel = mergeTablesIntoModel(new TableModel(), ...res.data); const latency = Date.now() - now; this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); this.onQuerySuccess(datasource.meta.id, queries); diff --git a/public/app/features/explore/Table.tsx b/public/app/features/explore/Table.tsx index e1c71fa55e4..fdb5d7de93a 100644 --- a/public/app/features/explore/Table.tsx +++ b/public/app/features/explore/Table.tsx @@ -5,6 +5,8 @@ import ReactTable from 'react-table'; import TableModel from 'app/core/table_model'; const EMPTY_TABLE = new TableModel(); +// Identify columns that contain values +const VALUE_REGEX = /^[Vv]alue #\d+/; interface TableProps { data: TableModel; @@ -34,6 +36,7 @@ export default class Table extends PureComponent { const columns = tableModel.columns.map(({ filterable, text }) => ({ Header: text, accessor: text, + className: VALUE_REGEX.test(text) ? 'text-right' : '', show: text !== 'Time', Cell: row => {row.value}, })); diff --git a/public/app/plugins/panel/table/specs/transformers.test.ts b/public/app/plugins/panel/table/specs/transformers.test.ts index 8d581b68842..49926aa00a8 100644 --- a/public/app/plugins/panel/table/specs/transformers.test.ts +++ b/public/app/plugins/panel/table/specs/transformers.test.ts @@ -143,24 +143,6 @@ describe('when transforming time series table', () => { }, ]; - const multipleQueriesDataDifferentLabels = [ - { - type: 'table', - columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }], - rows: [[time, 'Label Value 1', 42]], - }, - { - type: 'table', - columns: [{ text: 'Time' }, { text: 'Label Key 2' }, { text: 'Value #B' }], - rows: [[time, 'Label Value 2', 13]], - }, - { - type: 'table', - columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #C' }], - rows: [[time, 'Label Value 3', 7]], - }, - ]; - describe('getColumns', () => { it('should return data columns given a single query', () => { const columns = transformers[transform].getColumns(singleQueryData); @@ -177,16 +159,6 @@ describe('when transforming time series table', () => { expect(columns[3].text).toBe('Value #A'); expect(columns[4].text).toBe('Value #B'); }); - - it('should return the union of data columns given a multiple queries with different labels', () => { - const columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); - expect(columns[0].text).toBe('Time'); - expect(columns[1].text).toBe('Label Key 1'); - expect(columns[2].text).toBe('Value #A'); - expect(columns[3].text).toBe('Label Key 2'); - expect(columns[4].text).toBe('Value #B'); - expect(columns[5].text).toBe('Value #C'); - }); }); describe('transform', () => { @@ -237,26 +209,6 @@ describe('when transforming time series table', () => { expect(table.rows[1][4]).toBeUndefined(); expect(table.rows[1][5]).toBe(7); }); - - it('should return 2 rows for multiple queries with different label values', () => { - table = transformDataToTable(multipleQueriesDataDifferentLabels, panel); - expect(table.rows.length).toBe(2); - expect(table.columns.length).toBe(6); - - expect(table.rows[0][0]).toBe(time); - expect(table.rows[0][1]).toBe('Label Value 1'); - expect(table.rows[0][2]).toBe(42); - expect(table.rows[0][3]).toBe('Label Value 2'); - expect(table.rows[0][4]).toBe(13); - expect(table.rows[0][5]).toBeUndefined(); - - expect(table.rows[1][0]).toBe(time); - expect(table.rows[1][1]).toBe('Label Value 3'); - expect(table.rows[1][2]).toBeUndefined(); - expect(table.rows[1][3]).toBeUndefined(); - expect(table.rows[1][4]).toBeUndefined(); - expect(table.rows[1][5]).toBe(7); - }); }); }); }); diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 5a75fa7acf6..c56d385505b 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -import flatten from '../../../core/utils/flatten'; -import TimeSeries from '../../../core/time_series2'; -import TableModel from '../../../core/table_model'; +import flatten from 'app/core/utils/flatten'; +import TimeSeries from 'app/core/time_series2'; +import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; const transformers = {}; @@ -168,97 +168,7 @@ transformers['table'] = { }; } - // Single query returns data columns and rows as is - if (data.length === 1) { - model.columns = [...data[0].columns]; - model.rows = [...data[0].rows]; - return; - } - - // Track column indexes of union: name -> index - const columnNames = {}; - - // Union of all non-value columns - const columnsUnion = data.reduce((acc, series) => { - series.columns.forEach(col => { - const { text } = col; - if (columnNames[text] === undefined) { - columnNames[text] = acc.length; - acc.push(col); - } - }); - return acc; - }, []); - - // Map old column index to union index per series, e.g., - // given columnNames {A: 0, B: 1} and - // data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]] - const columnIndexMapper = data.map(series => series.columns.map(col => columnNames[col.text])); - - // Flatten rows of all series and adjust new column indexes - const flattenedRows = data.reduce((acc, series, seriesIndex) => { - const mapper = columnIndexMapper[seriesIndex]; - series.rows.forEach(row => { - const alteredRow = []; - // Shifting entries according to index mapper - mapper.forEach((to, from) => { - alteredRow[to] = row[from]; - }); - acc.push(alteredRow); - }); - return acc; - }, []); - - // Returns true if both rows have matching non-empty fields as well as matching - // indexes where one field is empty and the other is not - function areRowsMatching(columns, row, otherRow) { - let foundFieldToMatch = false; - for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { - if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) { - if (row[columnIndex] !== otherRow[columnIndex]) { - return false; - } - } else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) { - foundFieldToMatch = true; - } - } - return foundFieldToMatch; - } - - // Merge rows that have same values for columns - const mergedRows = {}; - const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => { - if (!mergedRows[rowIndex]) { - // Look from current row onwards - let offset = rowIndex + 1; - // More than one row can be merged into current row - while (offset < flattenedRows.length) { - // Find next row that could be merged - const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset); - if (match > -1) { - const matchedRow = flattenedRows[match]; - // Merge values from match into current row if there is a gap in the current row - for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) { - if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) { - row[columnIndex] = matchedRow[columnIndex]; - } - } - // Don't visit this row again - mergedRows[match] = matchedRow; - // Keep looking for more rows to merge - offset = match + 1; - } else { - // No match found, stop looking - break; - } - } - acc.push(row); - } - return acc; - }, []); - - model.columns = columnsUnion; - model.rows = compactedRows; + mergeTablesIntoModel(model, ...data); }, }; From 4b524fafa5e5e7164bbe67edc6317324c47a792d Mon Sep 17 00:00:00 2001 From: Dave Waters Date: Fri, 19 Oct 2018 16:33:23 -0400 Subject: [PATCH 03/33] initial work to add shortcut to toggle legend - generic --- public/app/core/components/help/help.ts | 1 + public/app/core/services/keybindingSrv.ts | 12 ++++++++++++ public/app/plugins/panel/graph/module.ts | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index eac47b6e0a2..8e8a5ed45d2 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -34,6 +34,7 @@ export class HelpCtrl { { keys: ['p', 's'], description: 'Open Panel Share Modal' }, { keys: ['p', 'd'], description: 'Duplicate Panel' }, { keys: ['p', 'r'], description: 'Remove Panel' }, + { keys: ['p', 'l'], description: 'Toggle panel legend' }, ], 'Time Range': [ { keys: ['t', 'z'], description: 'Zoom out time range' }, diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index f43dc96cd37..6fe57dfa77a 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -242,6 +242,18 @@ export class KeybindingSrv { } }); + // toggle panel legend + this.bind('p l', () => { + if (dashboard.meta.focusPanelId) { + const panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); + if (panelInfo.panel.legend) { + const panelRef = dashboard.getPanelById(dashboard.meta.focusPanelId); + panelRef.legend.show = !panelRef.legend.show; + panelRef.refresh(); + } + } + }); + // collapse all rows this.bind('d shift+c', () => { dashboard.collapseRows(); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 07256164c56..5878473b4e6 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -147,7 +147,7 @@ class GraphCtrl extends MetricsPanelCtrl { onInitPanelActions(actions) { actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' }); - actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' }); + actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()', shortcut: 'p l' }); } issueQueries(datasource) { From 30baaa48fe18f90042541956fb63a298ba45ee48 Mon Sep 17 00:00:00 2001 From: Emil Hessman Date: Sat, 20 Oct 2018 15:08:38 +0200 Subject: [PATCH 04/33] ux: remove duplicate placeholder attribute --- public/app/features/templating/partials/editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index ac4450c20a2..c4463972177 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -115,7 +115,7 @@
Values - +
From 3bb0b0a551f25e96fa7fb54624960cc8c9473524 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sun, 21 Oct 2018 12:51:21 +0800 Subject: [PATCH 05/33] Fix click-based selection of typeahead suggestion In short, the underlying problem appears to be the `onChange()` handler being triggered after handling the blur event. Since the contents have not actually changed this forces the typeahead state to reset which undesirably puts a stop to propagating the selected suggestion back up to get set. Related: #13604 --- public/app/features/explore/QueryField.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index c3c41b7ab17..ce0bcd71ed0 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -198,7 +198,7 @@ class QueryField extends React.PureComponent { From 22a0f3cf943a1465626ba2fbf0d073454611f1bb Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 23 Oct 2018 14:05:10 +0200 Subject: [PATCH 06/33] =?UTF-8?q?fix:=20Text=20box=20variables=20with=20em?= =?UTF-8?q?pty=20values=20should=20not=20be=20considered=20fa=E2=80=A6=20(?= =?UTF-8?q?#13791)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: text box template variable doesn't work properly without a default value --- .../templating/specs/template_srv.test.ts | 10 ++++++++++ public/app/features/templating/template_srv.ts | 15 ++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 7f5ff959216..d279029d64d 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -429,6 +429,11 @@ describe('templateSrv', () => { name: 'period', current: { value: '$__auto_interval_interval', text: 'auto' }, }, + { + type: 'textbox', + name: 'empty_on_init', + current: { value: '', text: '' }, + }, ]); _templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m'); _templateSrv.updateTemplateData(); @@ -438,6 +443,11 @@ describe('templateSrv', () => { const target = _templateSrv.replaceWithText('Server: $server, period: $period'); expect(target).toBe('Server: All, period: 13m'); }); + + it('should replace empty string-values with an empty string', () => { + const target = _templateSrv.replaceWithText('Hello $empty_on_init'); + expect(target).toBe('Hello '); + }); }); describe('built in interval variables', () => { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 70fd287402f..11d235f5a09 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -30,17 +30,14 @@ export class TemplateSrv { } updateTemplateData() { - this.index = {}; + const existsOrEmpty = value => value || value === ''; - for (let i = 0; i < this.variables.length; i++) { - const variable = this.variables[i]; - - if (!variable.current || (!variable.current.isNone && !variable.current.value)) { - continue; + this.index = this.variables.reduce((acc, currentValue) => { + if (currentValue.current && !currentValue.current.isNone && existsOrEmpty(currentValue.current.value)) { + acc[currentValue.name] = currentValue; } - - this.index[variable.name] = variable; - } + return acc; + }, {}); } variableInitialized(variable) { From 88e546128c5100dfb1911f6d2273da45239551e3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 23 Oct 2018 14:10:05 +0200 Subject: [PATCH 07/33] changelog: add notes about closing #13666 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10c712f34b..11a3be78fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) # 5.3.1 (2018-10-16) From f84db5107d400794e313c3b9be6af0cc0d462700 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 23 Oct 2018 14:17:57 +0200 Subject: [PATCH 08/33] changelog: add notes about closing #13633 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a3be78fa6..3301331ec90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm) * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: Fix check for invalid percentile statistics [#13633](https://github.com/grafana/grafana/issues/13633), thx [@apalaniuk](https://github.com/apalaniuk) * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) * **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) From 8a1e0cd83b3434a548aa7f9777031565b8b3057e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 23 Oct 2018 15:37:11 +0200 Subject: [PATCH 09/33] fix: kiosk url fix, fixes #13764 --- public/app/routes/GrafanaCtrl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index d6291c94a6f..737984193e1 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -88,7 +88,7 @@ function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) { break; } // 1 & true for legacy states - case 1: + case '1': case true: { body.removeClass('sidemenu-open'); body.addClass('view-mode--kiosk'); @@ -181,11 +181,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop switch (search.kiosk) { case 'tv': { - search.kiosk = 1; + search.kiosk = true; appEvents.emit('alert-success', ['Press ESC to exit Kiosk mode']); break; } - case 1: + case '1': case true: { delete search.kiosk; break; From 2e02a8c8550f656cd8895e283892eb722a3693e4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 22 Oct 2018 17:51:42 +0200 Subject: [PATCH 10/33] Explore: query transactions Existing querying was grouped together before handed over to the datasource. This slowed down result display to however long the slowest query took. - create one query transaction per result viewer (graph, table, etc.) and query row - track latencies for each transaction - show results as soon as they are being received - loading indicator on graph and query button to indicate that queries are still running and that results are incomplete - properly discard transactions when removing or changing queries --- public/app/core/utils/explore.test.ts | 9 +- public/app/features/explore/Explore.tsx | 429 ++++++++++++------ public/app/features/explore/Graph.test.tsx | 23 +- public/app/features/explore/Graph.tsx | 42 +- public/app/features/explore/QueryRows.tsx | 12 +- .../features/explore/QueryTransactions.tsx | 42 ++ public/app/features/explore/Table.tsx | 2 +- .../explore/__snapshots__/Graph.test.tsx.snap | 29 +- public/app/types/explore.ts | 31 +- public/sass/pages/_explore.scss | 76 +++- 10 files changed, 484 insertions(+), 211 deletions(-) create mode 100644 public/app/features/explore/QueryTransactions.tsx diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 915b47e14e2..04159e81164 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -8,23 +8,18 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { datasourceMissing: false, datasourceName: '', exploreDatasources: [], - graphResult: null, + graphRange: DEFAULT_RANGE, history: [], - latency: 0, - loading: false, - logsResult: null, queries: [], - queryErrors: [], queryHints: [], + queryTransactions: [], range: DEFAULT_RANGE, - requestOptions: null, showingGraph: true, showingLogs: true, showingTable: true, supportsGraph: null, supportsLogs: null, supportsTable: null, - tableResult: null, }; describe('state functions', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d7326a5bfd1..93c0847d6ed 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import Select from 'react-select'; +import _ from 'lodash'; -import { ExploreState, ExploreUrlState, Query } from 'app/types/explore'; +import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, Range } from 'app/types/explore'; import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; import store from 'app/core/store'; @@ -15,7 +16,6 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer' import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; -import ElapsedTime from './ElapsedTime'; import QueryRows from './QueryRows'; import Graph from './Graph'; import Logs from './Logs'; @@ -53,6 +53,25 @@ function makeTimeSeriesList(dataList, options) { }); } +/** + * Update the query history. Side-effect: store history in local storage + */ +function updateHistory(history: HistoryItem[], datasourceId: string, queries: string[]): HistoryItem[] { + const ts = Date.now(); + queries.forEach(query => { + history = [{ query, ts }, ...history]; + }); + + if (history.length > MAX_HISTORY_ITEMS) { + history = history.slice(0, MAX_HISTORY_ITEMS); + } + + // Combine all queries of a datasource type into one history + const historyKey = `grafana.explore.history.${datasourceId}`; + store.setObject(historyKey, history); + return history; +} + interface ExploreProps { datasourceSrv: any; onChangeSplit: (split: boolean, state?: ExploreState) => void; @@ -83,6 +102,7 @@ export class Explore extends React.PureComponent { } else { const { datasource, queries, range } = props.urlState as ExploreUrlState; initialQueries = ensureQueries(queries); + const initialRange = range || { ...DEFAULT_RANGE }; this.state = { datasource: null, datasourceError: null, @@ -90,23 +110,18 @@ export class Explore extends React.PureComponent { datasourceMissing: false, datasourceName: datasource, exploreDatasources: [], - graphResult: null, + graphRange: initialRange, history: [], - latency: 0, - loading: false, - logsResult: null, queries: initialQueries, - queryErrors: [], queryHints: [], - range: range || { ...DEFAULT_RANGE }, - requestOptions: null, + queryTransactions: [], + range: initialRange, showingGraph: true, showingLogs: true, showingTable: true, supportsGraph: null, supportsLogs: null, supportsTable: null, - tableResult: null, }; } this.queryExpressions = initialQueries.map(q => q.query); @@ -200,14 +215,30 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - const { queries } = this.state; + const { queries, queryTransactions } = this.state; + + // Local cache this.queryExpressions[index + 1] = ''; + + // Add row by generating new react key const nextQueries = [ ...queries.slice(0, index + 1), { query: '', key: generateQueryKey() }, ...queries.slice(index + 1), ]; - this.setState({ queries: nextQueries }); + + // Ongoing transactions need to update their row indices + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.rowIndex > index) { + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; + } + return qt; + }); + + this.setState({ queries: nextQueries, queryTransactions: nextQueryTransactions }); }; onChangeDatasource = async option => { @@ -215,12 +246,8 @@ export class Explore extends React.PureComponent { datasource: null, datasourceError: null, datasourceLoading: true, - graphResult: null, - latency: 0, - logsResult: null, - queryErrors: [], queryHints: [], - tableResult: null, + queryTransactions: [], }); const datasourceName = option.value; const datasource = await this.props.datasourceSrv.get(datasourceName); @@ -231,9 +258,9 @@ export class Explore extends React.PureComponent { // Keep current value in local cache this.queryExpressions[index] = value; - // Replace query row on override if (override) { - const { queries } = this.state; + // Replace query row + const { queries, queryTransactions } = this.state; const nextQuery: Query = { key: generateQueryKey(index), query: value, @@ -241,11 +268,14 @@ export class Explore extends React.PureComponent { const nextQueries = [...queries]; nextQueries[index] = nextQuery; + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + this.setState( { - queryErrors: [], - queryHints: [], queries: nextQueries, + queryHints: [], + queryTransactions: nextQueryTransactions, }, this.onSubmit ); @@ -264,13 +294,9 @@ export class Explore extends React.PureComponent { this.queryExpressions = ['']; this.setState( { - graphResult: null, - logsResult: null, - latency: 0, queries: ensureQueries(), - queryErrors: [], queryHints: [], - tableResult: null, + queryTransactions: [], }, this.saveState ); @@ -308,15 +334,18 @@ export class Explore extends React.PureComponent { }; onModifyQueries = (action: object, index?: number) => { - const { datasource, queries } = this.state; + const { datasource, queries, queryTransactions } = this.state; if (datasource && datasource.modifyQuery) { let nextQueries; + let nextQueryTransactions; if (index === undefined) { // Modify all queries nextQueries = queries.map((q, i) => ({ key: generateQueryKey(i), query: datasource.modifyQuery(this.queryExpressions[i], action), })); + // Discard all ongoing transactions + nextQueryTransactions = []; } else { // Modify query only at index nextQueries = [ @@ -327,20 +356,41 @@ export class Explore extends React.PureComponent { }, ...queries.slice(index + 1), ]; + // Discard transactions related to row query + nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); } this.queryExpressions = nextQueries.map(q => q.query); - this.setState({ queries: nextQueries }, () => this.onSubmit()); + this.setState( + { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }, + () => this.onSubmit() + ); } }; onRemoveQueryRow = index => { - const { queries } = this.state; + const { queries, queryTransactions } = this.state; if (queries.length <= 1) { return; } + // Remove from local cache + this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)]; + + // Remove row from react state const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; - this.queryExpressions = nextQueries.map(q => q.query); - this.setState({ queries: nextQueries }, () => this.onSubmit()); + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + + this.setState( + { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }, + () => this.onSubmit() + ); }; onSubmit = () => { @@ -349,7 +399,7 @@ export class Explore extends React.PureComponent { this.runTableQuery(); } if (showingGraph && supportsGraph) { - this.runGraphQuery(); + this.runGraphQueries(); } if (showingLogs && supportsLogs) { this.runLogsQuery(); @@ -357,32 +407,7 @@ export class Explore extends React.PureComponent { this.saveState(); }; - onQuerySuccess(datasourceId: string, queries: string[]): void { - // save queries to history - let { history } = this.state; - const { datasource } = this.state; - - if (datasource.meta.id !== datasourceId) { - // Navigated away, queries did not matter - return; - } - - const ts = Date.now(); - queries.forEach(query => { - history = [{ query, ts }, ...history]; - }); - - if (history.length > MAX_HISTORY_ITEMS) { - history = history.slice(0, MAX_HISTORY_ITEMS); - } - - // Combine all queries of a datasource type into one history - const historyKey = `grafana.explore.history.${datasourceId}`; - store.setObject(historyKey, history); - this.setState({ history }); - } - - buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { + buildQueryOptions(query: string, rowIndex: number, targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, range } = this.state; const resolution = this.el.offsetWidth; const absoluteRange = { @@ -390,90 +415,215 @@ export class Explore extends React.PureComponent { to: parseDate(range.to, true), }; const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval); - const targets = this.queryExpressions.map((q, i) => ({ - ...targetOptions, - // Target identifier is needed for table transformations - refId: i + 1, - expr: q, - })); + const targets = [ + { + ...targetOptions, + // Target identifier is needed for table transformations + refId: rowIndex + 1, + expr: query, + }, + ]; + + // Clone range for query request + const queryRange: Range = { ...range }; + return { interval, - range, targets, + range: queryRange, }; } - async runGraphQuery() { + startQueryTransaction(query: string, rowIndex: number, resultType: string, options: any): QueryTransaction { + const queryOptions = this.buildQueryOptions(query, rowIndex, options); + const transaction: QueryTransaction = { + query, + resultType, + rowIndex, + id: generateQueryKey(), + done: false, + latency: 0, + options: queryOptions, + }; + + // Using updater style because we might be modifying queryTransactions in quick succession + this.setState(state => { + const { queryTransactions } = state; + // Discarding existing transactions of same type + const remainingTransactions = queryTransactions.filter( + qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) + ); + + // Append new transaction + const nextQueryTransactions = [...remainingTransactions, transaction]; + + return { + queryHints: [], + queryTransactions: nextQueryTransactions, + }; + }); + + return transaction; + } + + completeQueryTransaction( + transactionId: string, + result: any, + latency: number, + hints: any[], + queries: string[], + datasourceId: string + ) { const { datasource } = this.state; + if (datasource.meta.id !== datasourceId) { + // Navigated away, queries did not matter + return; + } + + this.setState(state => { + const { history, queryTransactions } = state; + + // Transaction might have been discarded + if (!queryTransactions.find(qt => qt.id === transactionId)) { + return null; + } + + // Mark transactions as complete + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + latency, + result, + done: true, + }; + } + return qt; + }); + + const nextHistory = updateHistory(history, datasourceId, queries); + + return { + history: nextHistory, + queryHints: hints, + queryTransactions: nextQueryTransactions, + }; + }); + } + + failQueryTransaction(transactionId: string, error: string, datasourceId: string) { + const { datasource } = this.state; + if (datasource.meta.id !== datasourceId) { + // Navigated away, queries did not matter + return; + } + + this.setState(state => { + // Transaction might have been discarded + if (!state.queryTransactions.find(qt => qt.id === transactionId)) { + return null; + } + + // Mark transactions as complete + const nextQueryTransactions = state.queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + error, + done: true, + }; + } + return qt; + }); + + return { + queryTransactions: nextQueryTransactions, + }; + }); + } + + async runGraphQueries() { const queries = [...this.queryExpressions]; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] }); - const now = Date.now(); - const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true }); - try { - const res = await datasource.query(options); - const result = makeTimeSeriesList(res.data, options); - const queryHints = res.hints ? makeHints(res.hints) : []; - const latency = Date.now() - now; - this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options }); - this.onQuerySuccess(datasource.meta.id, queries); - } catch (response) { - console.error(response); - const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryErrors: [queryError] }); - } + const { datasource } = this.state; + const datasourceId = datasource.meta.id; + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + if (query) { + const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', { + format: 'time_series', + instant: false, + hinting: true, + }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = makeTimeSeriesList(res.data, transaction.options); + const queryHints = res.hints ? makeHints(res.hints) : []; + this.completeQueryTransaction(transaction.id, results, latency, queryHints, queries, datasourceId); + this.setState({ graphRange: transaction.options.range }); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.failQueryTransaction(transaction.id, queryError, datasourceId); + } + } + }); } async runTableQuery() { const queries = [...this.queryExpressions]; - const { datasource } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null }); - const now = Date.now(); - const options = this.buildQueryOptions({ - format: 'table', - instant: true, + const { datasource } = this.state; + const datasourceId = datasource.meta.id; + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + if (query) { + const transaction = this.startQueryTransaction(query, rowIndex, 'Table', { format: 'table', instant: true }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = mergeTablesIntoModel(new TableModel(), ...res.data); + this.completeQueryTransaction(transaction.id, results, latency, [], queries, datasourceId); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.failQueryTransaction(transaction.id, queryError, datasourceId); + } + } }); - try { - const res = await datasource.query(options); - const tableModel = mergeTablesIntoModel(new TableModel(), ...res.data); - const latency = Date.now() - now; - this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); - this.onQuerySuccess(datasource.meta.id, queries); - } catch (response) { - console.error(response); - const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryErrors: [queryError] }); - } } async runLogsQuery() { const queries = [...this.queryExpressions]; - const { datasource } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null }); - const now = Date.now(); - const options = this.buildQueryOptions({ - format: 'logs', + const { datasource } = this.state; + const datasourceId = datasource.meta.id; + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + if (query) { + const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = res.data; + this.completeQueryTransaction(transaction.id, results, latency, [], queries, datasourceId); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.failQueryTransaction(transaction.id, queryError, datasourceId); + } + } }); - - try { - const res = await datasource.query(options); - const logsData = res.data; - const latency = Date.now() - now; - this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options }); - this.onQuerySuccess(datasource.meta.id, queries); - } catch (response) { - console.error(response); - const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryErrors: [queryError] }); - } } request = url => { @@ -502,23 +652,18 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, + graphRange, history, - latency, - loading, - logsResult, queries, - queryErrors, queryHints, + queryTransactions, range, - requestOptions, showingGraph, showingLogs, showingTable, supportsGraph, supportsLogs, supportsTable, - tableResult, } = this.state; const showingBoth = showingGraph && showingTable; const graphHeight = showingBoth ? '200px' : '400px'; @@ -527,6 +672,17 @@ export class Explore extends React.PureComponent { const tableButtonActive = showingBoth || showingTable ? 'active' : ''; const exploreClass = split ? 'explore explore-split' : 'explore'; const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined; + const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); + const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); + const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + const graphResult = _.flatten( + queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result) + ); + const tableResult = queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done).map(qt => qt.result)[0]; + const logsResult = _.flatten( + queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done).map(qt => qt.result) + ); + const loading = queryTransactions.some(qt => !qt.done); return (
@@ -539,12 +695,12 @@ export class Explore extends React.PureComponent {
) : ( -
- -
- )} +
+ )} {!datasourceMissing ? (
1 ? `Value #${refId}` : 'Value'; + const valueText = resultCount > 1 || valueWithRefId ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 1459d05ac75..a3f60f2006b 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -91,7 +91,7 @@ height: 2px; position: relative; overflow: hidden; - background: $table-border; + background: $text-color-faint; margin: $panel-margin / 2; } From edba0880fc34999fe1eab38dc2d659753e1e036b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 09:08:16 +0200 Subject: [PATCH 17/33] changelog: add notes about closing #13764 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a998ad7ac6..9dcdb4becdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * **Cloudwatch**: Fix check for invalid percentile statistics [#13633](https://github.com/grafana/grafana/issues/13633), thx [@apalaniuk](https://github.com/apalaniuk) * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) * **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) +* **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) ### Minor From a121cd0e49d37caa42b7ba92427f9b05dedf8c1e Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 24 Oct 2018 11:08:15 +0200 Subject: [PATCH 18/33] Fix race condition on add/remove query row --- public/app/features/explore/Explore.tsx | 169 +++++++++++----------- public/app/features/explore/QueryRows.tsx | 16 +- 2 files changed, 95 insertions(+), 90 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index cc049a5c8bf..bac063116f1 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -33,16 +33,6 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; const MAX_HISTORY_ITEMS = 100; -function makeHints(transactions: QueryTransaction[]) { - const hintsByIndex = []; - transactions.forEach(qt => { - if (qt.hints && qt.hints.length > 0) { - hintsByIndex[qt.rowIndex] = qt.hints[0]; - } - }); - return hintsByIndex; -} - function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -222,30 +212,32 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - const { queries, queryTransactions } = this.state; - // Local cache this.queryExpressions[index + 1] = ''; - // Add row by generating new react key - const nextQueries = [ - ...queries.slice(0, index + 1), - { query: '', key: generateQueryKey() }, - ...queries.slice(index + 1), - ]; + this.setState(state => { + const { queries, queryTransactions } = state; - // Ongoing transactions need to update their row indices - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; - } - return qt; + // Add row by generating new react key + const nextQueries = [ + ...queries.slice(0, index + 1), + { query: '', key: generateQueryKey() }, + ...queries.slice(index + 1), + ]; + + // Ongoing transactions need to update their row indices + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.rowIndex > index) { + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; + } + return qt; + }); + + return { queries: nextQueries, queryTransactions: nextQueryTransactions }; }); - - this.setState({ queries: nextQueries, queryTransactions: nextQueryTransactions }); }; onChangeDatasource = async option => { @@ -265,25 +257,24 @@ export class Explore extends React.PureComponent { this.queryExpressions[index] = value; if (override) { - // Replace query row - const { queries, queryTransactions } = this.state; - const nextQuery: Query = { - key: generateQueryKey(index), - query: value, - }; - const nextQueries = [...queries]; - nextQueries[index] = nextQuery; + this.setState(state => { + // Replace query row + const { queries, queryTransactions } = state; + const nextQuery: Query = { + key: generateQueryKey(index), + query: value, + }; + const nextQueries = [...queries]; + nextQueries[index] = nextQuery; - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - this.setState( - { + return { queries: nextQueries, queryTransactions: nextQueryTransactions, - }, - this.onSubmit - ); + }; + }, this.onSubmit); } }; @@ -383,36 +374,39 @@ export class Explore extends React.PureComponent { }; onModifyQueries = (action: object, index?: number) => { - const { datasource, queries, queryTransactions } = this.state; + const { datasource } = this.state; if (datasource && datasource.modifyQuery) { - let nextQueries; - let nextQueryTransactions; - if (index === undefined) { - // Modify all queries - nextQueries = queries.map((q, i) => ({ - key: generateQueryKey(i), - query: datasource.modifyQuery(this.queryExpressions[i], action), - })); - // Discard all ongoing transactions - nextQueryTransactions = []; - } else { - // Modify query only at index - nextQueries = [ - ...queries.slice(0, index), - { - key: generateQueryKey(index), - query: datasource.modifyQuery(this.queryExpressions[index], action), - }, - ...queries.slice(index + 1), - ]; - // Discard transactions related to row query - nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - } - this.queryExpressions = nextQueries.map(q => q.query); this.setState( - { - queries: nextQueries, - queryTransactions: nextQueryTransactions, + state => { + const { queries, queryTransactions } = state; + let nextQueries; + let nextQueryTransactions; + if (index === undefined) { + // Modify all queries + nextQueries = queries.map((q, i) => ({ + key: generateQueryKey(i), + query: datasource.modifyQuery(this.queryExpressions[i], action), + })); + // Discard all ongoing transactions + nextQueryTransactions = []; + } else { + // Modify query only at index + nextQueries = [ + ...queries.slice(0, index), + { + key: generateQueryKey(index), + query: datasource.modifyQuery(this.queryExpressions[index], action), + }, + ...queries.slice(index + 1), + ]; + // Discard transactions related to row query + nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + } + this.queryExpressions = nextQueries.map(q => q.query); + return { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }; }, () => this.onSubmit() ); @@ -420,23 +414,25 @@ export class Explore extends React.PureComponent { }; onRemoveQueryRow = index => { - const { queries, queryTransactions } = this.state; - if (queries.length <= 1) { - return; - } // Remove from local cache this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)]; - // Remove row from react state - const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; - - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - this.setState( - { - queries: nextQueries, - queryTransactions: nextQueryTransactions, + state => { + const { queries, queryTransactions } = state; + if (queries.length <= 1) { + return null; + } + // Remove row from react state + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + + return { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }; }, () => this.onSubmit() ); @@ -708,6 +704,7 @@ export class Explore extends React.PureComponent { // Copy state, but copy queries including modifications return { ...this.state, + queryTransactions: [], queries: ensureQueries(this.queryExpressions.map(query => ({ query }))), }; } @@ -758,7 +755,6 @@ export class Explore extends React.PureComponent { queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done).map(qt => qt.result) ); const loading = queryTransactions.some(qt => !qt.done); - const queryHints = makeHints(queryTransactions); return (
@@ -837,7 +833,6 @@ export class Explore extends React.PureComponent { qt.hints && qt.hints.length > 0); + if (transaction) { + return transaction.hints[0]; + } + return undefined; +} + class QueryRow extends PureComponent { onChangeQuery = (value, override?: boolean) => { const { index, onChangeQuery } = this.props; @@ -45,8 +55,9 @@ class QueryRow extends PureComponent { }; render() { - const { history, query, queryHint, request, supportsLogs, transactions } = this.props; + const { history, query, request, supportsLogs, transactions } = this.props; const transactionWithError = transactions.find(t => t.error); + const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; return (
@@ -56,7 +67,7 @@ class QueryRow extends PureComponent {
{ index={index} query={q.query} transactions={transactions.filter(t => t.rowIndex === index)} - queryHint={queryHints[index]} {...handlers} /> ))} From c5af0bf1c5f625c5f1ba13781ff89d53f1724a85 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 24 Oct 2018 11:18:49 +0200 Subject: [PATCH 19/33] Resource type filter (#13784) * stackdriver: add resource type to filter and group bys * stackdriver: remove not used param * stackdriver: refactor filter and group by code * stackdriver: remove resource type if its already in filter list * stackdriver: remove debug logging * stackdriver: remove more debug logging * stackdriver: append resource type to legend name if there are more than one type present in the response * stackdriver: only make new request if filter has real value * stackdriver: format legend support for resource type * stackdriver: add resource type to documentation * stackdriver: not returning promise from query function * stackdriver: fix refactoring bug * stackdriver: remove not used import --- .../features/datasources/stackdriver.md | 10 +++ pkg/tsdb/stackdriver/stackdriver.go | 23 ++++- .../datasource/stackdriver/datasource.ts | 50 +++++------ .../datasource/stackdriver/filter_segments.ts | 2 +- .../stackdriver/partials/query.filter.html | 2 +- .../datasource/stackdriver/query_ctrl.ts | 1 - .../stackdriver/query_filter_ctrl.ts | 88 ++++++++++++------- 7 files changed, 113 insertions(+), 63 deletions(-) diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index 3ae2ed3df40..d19dbe4ea50 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -156,6 +156,16 @@ Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}` Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod` +It is also possible to resolve the name of the Monitored Resource Type. + +| Alias Pattern Format | Description | Example Result | +| ------------------------ | ------------------------------------------------| ---------------- | +| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` | + +Example Alias By: `{{resource.type}} - {{metric.type}}` + +Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time` + ## Templating Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 8b903ba0113..b33d33fb41c 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -355,11 +355,21 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error { metricLabels := make(map[string][]string) resourceLabels := make(map[string][]string) + var resourceTypes []string + + for _, series := range data.TimeSeries { + if !containsLabel(resourceTypes, series.Resource.Type) { + resourceTypes = append(resourceTypes, series.Resource.Type) + } + } for _, series := range data.TimeSeries { points := make([]tsdb.TimePoint, 0) defaultMetricName := series.Metric.Type + if len(resourceTypes) > 1 { + defaultMetricName += " " + series.Resource.Type + } for key, value := range series.Metric.Labels { if !containsLabel(metricLabels[key], value) { @@ -403,7 +413,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000)) } - metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query) + metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query) queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ Name: metricName, @@ -429,7 +439,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i) additionalLabels := map[string]string{"bucket": bucketBound} buckets[i] = &tsdb.TimeSeries{ - Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query), + Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query), Points: make([]tsdb.TimePoint, 0), } if maxKey < i { @@ -445,7 +455,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i) additionalLabels := map[string]string{"bucket": bucketBound} buckets[i] = &tsdb.TimeSeries{ - Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query), + Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query), Points: make([]tsdb.TimePoint, 0), } } @@ -460,6 +470,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta queryRes.Meta.Set("resourceLabels", resourceLabels) queryRes.Meta.Set("metricLabels", metricLabels) queryRes.Meta.Set("groupBys", query.GroupBys) + queryRes.Meta.Set("resourceTypes", resourceTypes) return nil } @@ -473,7 +484,7 @@ func containsLabel(labels []string, newLabel string) bool { return false } -func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string { +func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string { if query.AliasBy == "" { return defaultMetricName } @@ -487,6 +498,10 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels return []byte(metricType) } + if metaPartName == "resource.type" && resourceType != "" { + return []byte(resourceType) + } + metricPart := replaceWithMetricPart(metaPartName, metricType) if metricPart != nil { diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index cda952c23b9..4a81eb8a619 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -107,34 +107,32 @@ export default class StackdriverDatasource { } async query(options) { - this.queryPromise = new Promise(async resolve => { - const result = []; - const data = await this.getTimeSeries(options); - if (data.results) { - Object['values'](data.results).forEach(queryRes => { - if (!queryRes.series) { - return; + const result = []; + const data = await this.getTimeSeries(options); + if (data.results) { + Object['values'](data.results).forEach(queryRes => { + if (!queryRes.series) { + return; + } + this.projectName = queryRes.meta.defaultProject; + const unit = this.resolvePanelUnitFromTargets(options.targets); + queryRes.series.forEach(series => { + let timeSerie: any = { + target: series.name, + datapoints: series.points, + refId: queryRes.refId, + meta: queryRes.meta, + }; + if (unit) { + timeSerie = { ...timeSerie, unit }; } - this.projectName = queryRes.meta.defaultProject; - const unit = this.resolvePanelUnitFromTargets(options.targets); - queryRes.series.forEach(series => { - let timeSerie: any = { - target: series.name, - datapoints: series.points, - refId: queryRes.refId, - meta: queryRes.meta, - }; - if (unit) { - timeSerie = { ...timeSerie, unit }; - } - result.push(timeSerie); - }); + result.push(timeSerie); }); - } - - resolve({ data: result }); - }); - return this.queryPromise; + }); + return { data: result }; + } else { + return { data: [] }; + } } async annotationQuery(options) { diff --git a/public/app/plugins/datasource/stackdriver/filter_segments.ts b/public/app/plugins/datasource/stackdriver/filter_segments.ts index 9eb27f31975..5adb56e2fcf 100644 --- a/public/app/plugins/datasource/stackdriver/filter_segments.ts +++ b/public/app/plugins/datasource/stackdriver/filter_segments.ts @@ -44,7 +44,7 @@ export class FilterSegments { this.removeSegment.value = DefaultRemoveFilterValue; return Promise.resolve([this.removeSegment]); } else { - return this.getFilterKeysFunc(); + return this.getFilterKeysFunc(segment, DefaultRemoveFilterValue); } } diff --git a/public/app/plugins/datasource/stackdriver/partials/query.filter.html b/public/app/plugins/datasource/stackdriver/partials/query.filter.html index 9ec59005a0b..5043161c492 100644 --- a/public/app/plugins/datasource/stackdriver/partials/query.filter.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.filter.html @@ -28,7 +28,7 @@
Group By
- +
diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 0996ce82919..3a1961eb14e 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -95,6 +95,5 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.lastQueryError = jsonBody.error.message; } } - console.error(err); } } diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index df5fb0f2965..4c383e5d09e 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -1,6 +1,6 @@ import coreModule from 'app/core/core_module'; import _ from 'lodash'; -import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; +import { FilterSegments } from './filter_segments'; import appEvents from 'app/core/app_events'; export class StackdriverFilter { @@ -26,8 +26,10 @@ export class StackdriverFilter { export class StackdriverFilterCtrl { metricLabels: { [key: string]: string[] }; resourceLabels: { [key: string]: string[] }; + resourceTypes: string[]; defaultRemoveGroupByValue = '-- remove group by --'; + resourceTypeValue = 'resource.type'; loadLabelsPromise: Promise; service: string; @@ -72,7 +74,7 @@ export class StackdriverFilterCtrl { this.filterSegments = new FilterSegments( this.uiSegmentSrv, this.target, - this.getGroupBys.bind(this, null, null, DefaultRemoveFilterValue, false), + this.getFilterKeys.bind(this), this.getFilterValues.bind(this) ); this.filterSegments.buildSegmentModel(); @@ -151,6 +153,7 @@ export class StackdriverFilterCtrl { const data = await this.datasource.getLabels(this.target.metricType, this.target.refId); this.metricLabels = data.results[this.target.refId].meta.metricLabels; this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; + this.resourceTypes = data.results[this.target.refId].meta.resourceTypes; resolve(); } catch (error) { if (error.data && error.data.message) { @@ -191,45 +194,66 @@ export class StackdriverFilterCtrl { this.$rootScope.$broadcast('metricTypeChanged'); } - async getGroupBys(segment, index, removeText?: string, removeUsed = true) { + async createLabelKeyElements() { await this.loadLabelsPromise; - const metricLabels = Object.keys(this.metricLabels || {}) - .filter(ml => { - if (!removeUsed) { - return true; - } - return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1; - }) - .map(l => { - return this.uiSegmentSrv.newSegment({ - value: `metric.label.${l}`, - expandable: false, - }); + let elements = Object.keys(this.metricLabels || {}).map(l => { + return this.uiSegmentSrv.newSegment({ + value: `metric.label.${l}`, + expandable: false, }); + }); - const resourceLabels = Object.keys(this.resourceLabels || {}) - .filter(ml => { - if (!removeUsed) { - return true; - } - - return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1; - }) - .map(l => { + elements = [ + ...elements, + ...Object.keys(this.resourceLabels || {}).map(l => { return this.uiSegmentSrv.newSegment({ value: `resource.label.${l}`, expandable: false, }); - }); + }), + ]; - const noValueOrPlusButton = !segment || segment.type === 'plus-button'; - if (noValueOrPlusButton && metricLabels.length === 0 && resourceLabels.length === 0) { - return Promise.resolve([]); + if (this.resourceTypes && this.resourceTypes.length > 0) { + elements = [ + ...elements, + this.uiSegmentSrv.newSegment({ + value: this.resourceTypeValue, + expandable: false, + }), + ]; } - this.removeSegment.value = removeText || this.defaultRemoveGroupByValue; - return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]); + return elements; + } + + async getFilterKeys(segment, removeText?: string) { + let elements = await this.createLabelKeyElements(); + + if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) { + elements = elements.filter(e => e.value !== this.resourceTypeValue); + } + + const noValueOrPlusButton = !segment || segment.type === 'plus-button'; + if (noValueOrPlusButton && elements.length === 0) { + return []; + } + + this.removeSegment.value = removeText; + return [...elements, this.removeSegment]; + } + + async getGroupBys(segment) { + let elements = await this.createLabelKeyElements(); + + elements = elements.filter(e => this.target.aggregation.groupBys.indexOf(e.value) === -1); + const noValueOrPlusButton = !segment || segment.type === 'plus-button'; + if (noValueOrPlusButton && elements.length === 0) { + return []; + } + + this.removeSegment.value = this.defaultRemoveGroupByValue; + return [...elements, this.removeSegment]; } groupByChanged(segment, index) { @@ -273,6 +297,10 @@ export class StackdriverFilterCtrl { return this.resourceLabels[shortKey]; } + if (filterKey === this.resourceTypeValue) { + return this.resourceTypes; + } + return []; } From 00d6707045bbdc893a540aebb0e526451c726952 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 11:20:30 +0200 Subject: [PATCH 20/33] docs: fix tutorials index page. Fixes #13799 --- docs/sources/tutorials/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/tutorials/index.md b/docs/sources/tutorials/index.md index cb11940c6dd..08c5a3683f6 100644 --- a/docs/sources/tutorials/index.md +++ b/docs/sources/tutorials/index.md @@ -1,5 +1,6 @@ +++ title = "Tutorials" +type = "docs" [menu.docs] identifier = "tutorials" weight = 6 From 57a7007421508ab34c49c4459057b25d2aa7e27b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 24 Oct 2018 11:21:42 +0200 Subject: [PATCH 21/33] changelog: adds note for #13691 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcdb4becdf..b375ad020da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) * **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) +* **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691) ### Minor From d6ff16fe725eb3bfc8b376f612ce0d94cf2df651 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 24 Oct 2018 11:25:49 +0200 Subject: [PATCH 22/33] Explore: fix graph resize on window resize - resize HOC wrapper only resized first child which in case of large graphs was the warning button - moved warning button inside the panel parent --- public/app/features/explore/Graph.tsx | 12 +- .../explore/__snapshots__/Graph.test.tsx.snap | 1792 ++++++++--------- 2 files changed, 898 insertions(+), 906 deletions(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 65dac2953e2..d57f8d49a43 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -168,7 +168,8 @@ export class Graph extends PureComponent { const data = this.getGraphData(); return ( - <> +
+ {loading &&
} {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && ( @@ -180,12 +181,9 @@ export class Graph extends PureComponent { }`}
)} -
- {loading &&
} -
- -
- +
+ +
); } } diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index 243fccb2e5f..fd2010a76d3 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -1,468 +1,468 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` - +
-
+ - -
- + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/label/:name/values", + "instance": "localhost:9090", + "job": "prometheus", + "le": "3", + }, + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/label/:name/values", + "instance": "localhost:9090", + "job": "prometheus", + "le": "60", + }, + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/label/:name/values", + "instance": "localhost:9090", + "job": "prometheus", + "le": "8", + }, + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "+Inf", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "0.1", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "0.4", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "1", + }, + "values": Array [ + Array [ + 1537847900, + "953", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "120", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "20", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "3", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "60", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "8", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + ] + } + /> +
`; exports[`Render should render component with disclaimer 1`] = ` - +
@@ -478,504 +478,498 @@ exports[`Render should render component with disclaimer 1`] = `
-
+ - -
- + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/label/:name/values", + "instance": "localhost:9090", + "job": "prometheus", + "le": "60", + }, + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/label/:name/values", + "instance": "localhost:9090", + "job": "prometheus", + "le": "8", + }, + "values": Array [ + Array [ + 1537858100, + "16", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "+Inf", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "0.1", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "0.4", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "1", + }, + "values": Array [ + Array [ + 1537847900, + "953", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "120", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "20", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "3", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "60", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/metrics", + "instance": "localhost:9090", + "job": "prometheus", + "le": "8", + }, + "values": Array [ + Array [ + 1537858060, + "1195", + ], + Array [ + 1537858080, + "1195", + ], + Array [ + 1537858100, + "1195", + ], + ], + }, + Object { + "metric": Object { + "__name__": "prometheus_http_request_duration_seconds_bucket", + "handler": "/query", + "instance": "localhost:9090", + "job": "prometheus", + "le": "+Inf", + }, + "values": Array [ + Array [ + 1537858100, + "55", + ], + Array [ + 1537861960, + "1", + ], + Array [ + 1537861980, + "1", + ], + ], + }, + ] + } + /> +
`; exports[`Render should show query return no time series 1`] = ` - +
-
- -
- + } + /> + +
`; From 758a5ecf514da6277cec59d572f3514bf71bde76 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 11:55:43 +0200 Subject: [PATCH 23/33] docs: fix tutorials index page. Fixes #13799 --- docs/sources/tutorials/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sources/tutorials/index.md b/docs/sources/tutorials/index.md index 08c5a3683f6..90410e901d3 100644 --- a/docs/sources/tutorials/index.md +++ b/docs/sources/tutorials/index.md @@ -12,7 +12,11 @@ This section of the docs contains a series for tutorials and stack setup guides. ## Articles -- [How to integrate Hubot with Grafana](hubot_howto.md) +- [Running Grafana behind a reverse proxy]({{< relref "behind_proxy.md" >}}) +- [API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization]({{< relref "api_org_token_howto.md" >}}) +- [How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows]({{< relref "iis.md" >}}) +- [How to integrate Hubot with Grafana]({{< relref "hubot_howto.md" >}}) +- [How to setup Grafana for high availability]({{< relref "ha_setup.md" >}}) ## External links From 38c155403ecd497e94f19d30f0c4b0a902726078 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 24 Oct 2018 12:06:09 +0200 Subject: [PATCH 24/33] =?UTF-8?q?Move=20the=20variable=20regex=20to=20cons?= =?UTF-8?q?tants=20to=20make=20sure=20we=20use=20the=20same=20reg=E2=80=A6?= =?UTF-8?q?=20(#13801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templating/specs/variable.test.ts | 15 ++++++++ .../app/features/templating/template_srv.ts | 9 ++--- public/app/features/templating/variable.ts | 36 +++++++++++++------ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/public/app/features/templating/specs/variable.test.ts b/public/app/features/templating/specs/variable.test.ts index 6d5e88fa4bd..83f4af8bca9 100644 --- a/public/app/features/templating/specs/variable.test.ts +++ b/public/app/features/templating/specs/variable.test.ts @@ -22,6 +22,11 @@ describe('containsVariable', () => { expect(contains).toBe(true); }); + it('should find it with [[var:option]] syntax', () => { + const contains = containsVariable('this.[[test:csv]].filters', 'test'); + expect(contains).toBe(true); + }); + it('should find it when part of segment', () => { const contains = containsVariable('metrics.$env.$group-*', 'group'); expect(contains).toBe(true); @@ -36,6 +41,16 @@ describe('containsVariable', () => { const contains = containsVariable('asd', 'asd2.$env', 'env'); expect(contains).toBe(true); }); + + it('should find it with ${var} syntax', () => { + const contains = containsVariable('this.${test}.filters', 'test'); + expect(contains).toBe(true); + }); + + it('should find it with ${var:option} syntax', () => { + const contains = containsVariable('this.${test:csv}.filters', 'test'); + expect(contains).toBe(true); + }); }); }); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 11d235f5a09..61326ad63ec 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -1,5 +1,6 @@ import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; +import { variableRegex } from 'app/features/templating/variable'; function luceneEscape(value) { return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1'); @@ -8,13 +9,7 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; - /* - * This regex matches 3 types of variable reference with an optional format specifier - * \$(\w+) $var1 - * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] - * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} - */ - private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; + private regex = variableRegex; private index = {}; private grafanaVariables = {}; private builtIns = {}; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 412426fb294..1994e86eff0 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -1,6 +1,19 @@ -import kbn from 'app/core/utils/kbn'; import { assignModelProperties } from 'app/core/utils/model_utils'; +/* + * This regex matches 3 types of variable reference with an optional format specifier + * \$(\w+) $var1 + * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} + */ +export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; + +// Helper function since lastIndex is not reset +export const variableRegexExec = (variableString: string) => { + variableRegex.lastIndex = 0; + return variableRegex.exec(variableString); +}; + export interface Variable { setValue(option); updateOptions(); @@ -14,15 +27,16 @@ export let variableTypes = {}; export { assignModelProperties }; export function containsVariable(...args: any[]) { - let variableName = args[args.length - 1]; - let str = args[0] || ''; + const variableName = args[args.length - 1]; + const variableString = args.slice(0, -1).join(' '); + const matches = variableString.match(variableRegex); + const isMatchingVariable = + matches !== null + ? matches.find(match => { + const varMatch = variableRegexExec(match); + return varMatch !== null && varMatch.indexOf(variableName) > -1; + }) + : false; - for (let i = 1; i < args.length - 1; i++) { - str += ' ' + args[i] || ''; - } - - variableName = kbn.regexEscape(variableName); - const findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g'); - const match = findVarRegex.exec(str); - return match !== null; + return !!isMatchingVariable; } From 27cbcbcbc25ffa0004226c54405dc6949d493cd0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 12:07:27 +0200 Subject: [PATCH 25/33] changelog: update [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b375ad020da..cd4bd9209a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,10 @@ * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Fix check for invalid percentile statistics [#13633](https://github.com/grafana/grafana/issues/13633), thx [@apalaniuk](https://github.com/apalaniuk) * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) +* **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691) * **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) * **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) -* **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691) ### Minor From 1fbef171c58177847886f9fc9ed7b246abfaca30 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 12:08:33 +0200 Subject: [PATCH 26/33] changelog: add notes about closing #13600 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4bd9209a3..cffba42e009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) * **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691) * **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666) +* **Variables**: Fix variable dependency check when using `${var}` format [#13600](https://github.com/grafana/grafana/issues/13600) * **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) From b880f8d5487c31dfb77b2b60715cb42fe2989ed1 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 24 Oct 2018 13:27:09 +0200 Subject: [PATCH 27/33] Update the regex-matching in templateSrv to work with the new variable-syntax and be more flexible to regex-changes #13804 --- .../templating/specs/template_srv.test.ts | 32 ++++++++++++++++++- .../app/features/templating/template_srv.ts | 3 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index d279029d64d..7805341d1a2 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -286,10 +286,40 @@ describe('templateSrv', () => { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); - it('should return true if exists', () => { + it('should return true if $test exists', () => { const result = _templateSrv.variableExists('$test'); expect(result).toBe(true); }); + + it('should return true if $test exists in string', () => { + const result = _templateSrv.variableExists('something $test something'); + expect(result).toBe(true); + }); + + it('should return true if [[test]] exists in string', () => { + const result = _templateSrv.variableExists('something [[test]] something'); + expect(result).toBe(true); + }); + + it('should return true if [[test:csv]] exists in string', () => { + const result = _templateSrv.variableExists('something [[test:csv]] something'); + expect(result).toBe(true); + }); + + it('should return true if ${test} exists in string', () => { + const result = _templateSrv.variableExists('something ${test} something'); + expect(result).toBe(true); + }); + + it('should return true if ${test:raw} exists in string', () => { + const result = _templateSrv.variableExists('something ${test:raw} something'); + expect(result).toBe(true); + }); + + it('should return null if there are no variables in string', () => { + const result = _templateSrv.variableExists('string without variables'); + expect(result).toBe(null); + }); }); describe('can highlight variables in string', () => { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 61326ad63ec..0db7b8e77e0 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -136,7 +136,8 @@ export class TemplateSrv { if (!match) { return null; } - return match[1] || match[2]; + const variableName = match.slice(1).find(match => match !== undefined); + return variableName; } variableExists(expression) { From 0a9bfc552940ec6231022552cee99e13ef105f68 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 24 Oct 2018 13:32:09 +0200 Subject: [PATCH 28/33] delete provisioning meta data when deleting folder prior to this fix Grafana didnt delete meta data about the provisioned dashboard in `dashboard_provisioning` which means that the dashboard wasn't inserted into Grafana again if the folder was delete within Grafana. closes #13280 --- pkg/services/sqlstore/dashboard.go | 7 +++- .../sqlstore/dashboard_provisioning_test.go | 34 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index e43279208e7..1b853d17b5f 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -320,13 +320,18 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { "DELETE FROM dashboard WHERE id = ?", "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", "DELETE FROM dashboard_version WHERE dashboard_id = ?", - "DELETE FROM dashboard WHERE folder_id = ?", "DELETE FROM annotation WHERE dashboard_id = ?", "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", } + if dashboard.IsFolder { + deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)") + deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?") + } + for _, sql := range deletes { _, err := sess.Exec(sql, dashboard.Id) + if err != nil { return err } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 7ef45df3152..1b7a3976727 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -13,17 +13,30 @@ func TestDashboardProvisioningTest(t *testing.T) { Convey("Testing Dashboard provisioning", t, func() { InitTestDB(t) - saveDashboardCmd := &models.SaveDashboardCommand{ + folderCmd := &models.SaveDashboardCommand{ OrgId: 1, FolderId: 0, - IsFolder: false, + IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dashboard", }), } - Convey("Saving dashboards with extras", func() { + err := SaveDashboard(folderCmd) + So(err, ShouldBeNil) + + saveDashboardCmd := &models.SaveDashboardCommand{ + OrgId: 1, + IsFolder: false, + FolderId: folderCmd.Result.Id, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dashboard", + }), + } + + Convey("Saving dashboards with provisioning meta data", func() { now := time.Now() cmd := &models.SaveProvisionedDashboardCommand{ @@ -67,6 +80,21 @@ func TestDashboardProvisioningTest(t *testing.T) { So(err, ShouldBeNil) So(query.Result, ShouldBeFalse) }) + + Convey("Deleteing folder should delete provision meta data", func() { + deleteCmd := &models.DeleteDashboardCommand{ + Id: folderCmd.Result.Id, + OrgId: 1, + } + + So(DeleteDashboard(deleteCmd), ShouldBeNil) + + query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} + + err = GetProvisionedDataByDashboardId(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeFalse) + }) }) }) } From 3a1ece537c979f8c1be81025ad5d00e9a95593d1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 13:58:37 +0200 Subject: [PATCH 29/33] changelog: add notes about closing #13280 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cffba42e009..a6c8580957b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Variables**: Fix variable dependency check when using `${var}` format [#13600](https://github.com/grafana/grafana/issues/13600) * **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764) * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) +* **Provisioning**: Fix deleting provisioned dashboard folder should cleanup provisioning meta data [#13280](https://github.com/grafana/grafana/issues/13280) ### Minor From 63c13198e36ac2d6ba81757f9bcc5b0052e49702 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 14:36:26 +0200 Subject: [PATCH 30/33] changelog: update [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c8580957b..778da9cd499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited) -# 5.3.2 (unreleased) +# 5.3.2 (2018-10-24) * **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm) * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) From 259c243723a3157284b237a71fc52160172b7c5d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 24 Oct 2018 14:37:28 +0200 Subject: [PATCH 31/33] update latest.json to latest stable version [skip ci] --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 4355e9a64b7..992af0a8336 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.3.1", - "testing": "5.3.1" + "stable": "5.3.2", + "testing": "5.3.2" } From eb255520cbfc979ff9ba2c22dedc5a8e1e93ac62 Mon Sep 17 00:00:00 2001 From: Yuan Liu Date: Wed, 24 Oct 2018 22:28:59 +0800 Subject: [PATCH 32/33] fix dingding doc error --- docs/sources/alerting/notifications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 307af1ee15e..b232ee78f27 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -140,7 +140,7 @@ In DingTalk PC Client: 6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the grafana Dingtalk setting page and then click "finish". -Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported. +Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported. ### Kafka From defccb5ab3591414f587b3e3414743fb07065ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 25 Oct 2018 10:32:23 +0200 Subject: [PATCH 33/33] fix panel solo size --- public/sass/pages/_dashboard.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index d9ab29cc91c..795766a22de 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -21,6 +21,9 @@ div.flot-text { height: 100%; &--solo { + position: fixed; + bottom: 0; + right: 0; margin: 0; .panel-container { border: none;