From e832f91fb6331ed76ae7fa94e714544c0be516ec Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 13:37:15 +0200 Subject: [PATCH 01/14] Fix initial state in split explore - remove `edited` from query state to reset queries - clear more properties in state --- public/app/containers/Explore/Explore.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 9620ac4f91b..d161e7689cf 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -207,6 +207,7 @@ export class Explore extends React.Component { datasourceError: null, datasourceLoading: true, graphResult: null, + latency: 0, logsResult: null, queryErrors: [], queryHints: [], @@ -254,7 +255,10 @@ export class Explore extends React.Component { this.setState({ graphResult: null, logsResult: null, + latency: 0, queries: ensureQueries(), + queryErrors: [], + queryHints: [], tableResult: null, }); }; @@ -276,8 +280,10 @@ export class Explore extends React.Component { onClickSplit = () => { const { onChangeSplit } = this.props; + const state = { ...this.state }; + state.queries = state.queries.map(({ edited, ...rest }) => rest); if (onChangeSplit) { - onChangeSplit(true, this.state); + onChangeSplit(true, state); } }; From a0fbe3c296efb2082ffb9d3fd3481d6fd1fc6a41 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 14:45:09 +0200 Subject: [PATCH 02/14] Explore: Filter out existing labels in label suggestions - a valid selector returns all possible labels from the series API - we only want to suggest the label keys that are not part of the selector yet --- .../Explore/PromQueryField.jest.tsx | 19 ++++++ .../app/containers/Explore/PromQueryField.tsx | 16 +++-- .../Explore/utils/prometheus.jest.ts | 62 ++++++++++++++----- .../containers/Explore/utils/prometheus.ts | 17 ++--- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx index 350a529c89e..c82a1cd448f 100644 --- a/public/app/containers/Explore/PromQueryField.jest.tsx +++ b/public/app/containers/Explore/PromQueryField.jest.tsx @@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); + it('returns label suggestions on label context but leaves out labels that already exist', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const value = Plain.deserialize('{job="foo",}'); + const range = value.selection.merge({ + anchorOffset: 11, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.getTypeahead({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + it('returns a refresher on label context and unavailable metric', () => { const instance = shallow( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..0991f08429a 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import BracesPlugin from './slate-plugins/braces'; import RunnerPlugin from './slate-plugins/runner'; -import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus'; +import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus'; import TypeaheadField, { Suggestion, @@ -328,7 +328,7 @@ class PromQueryField extends React.Component -1; + const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { // Label values @@ -374,8 +377,11 @@ class PromQueryField extends React.Component 0) { + context = 'context-labels'; + suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); + } } } diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.jest.ts index febaecc29b5..d12d28c6bc9 100644 --- a/public/app/containers/Explore/utils/prometheus.jest.ts +++ b/public/app/containers/Explore/utils/prometheus.jest.ts @@ -1,33 +1,61 @@ -import { getCleanSelector } from './prometheus'; +import { parseSelector } from './prometheus'; + +describe('parseSelector()', () => { + let parsed; -describe('getCleanSelector()', () => { it('returns a clean selector from an empty selector', () => { - expect(getCleanSelector('{}', 1)).toBe('{}'); + parsed = parseSelector('{}', 1); + expect(parsed.selector).toBe('{}'); + expect(parsed.labelKeys).toEqual([]); }); + it('throws if selector is broken', () => { - expect(() => getCleanSelector('{foo')).toThrow(); + expect(() => parseSelector('{foo')).toThrow(); }); + it('returns the selector sorted by label key', () => { - expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}'); + parsed = parseSelector('{foo="bar"}'); + expect(parsed.selector).toBe('{foo="bar"}'); + expect(parsed.labelKeys).toEqual(['foo']); + + parsed = parseSelector('{foo="bar",baz="xx"}'); + expect(parsed.selector).toBe('{baz="xx",foo="bar"}'); }); + it('returns a clean selector from an incomplete one', () => { - expect(getCleanSelector('{foo}')).toBe('{}'); - expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}'); + parsed = parseSelector('{foo}'); + expect(parsed.selector).toBe('{}'); + + parsed = parseSelector('{foo="bar",baz}'); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar",baz="}'); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('throws if not inside a selector', () => { - expect(() => getCleanSelector('foo{}', 0)).toThrow(); - expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow(); + expect(() => parseSelector('foo{}', 0)).toThrow(); + expect(() => parseSelector('foo{} + bar{}', 5)).toThrow(); }); + it('returns the selector nearest to the cursor offset', () => { - expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); - expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}'); + expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); + + parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('returns a selector with metric if metric is given', () => { - expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}'); - expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}'); + parsed = parseSelector('bar{foo}', 4); + expect(parsed.selector).toBe('{__name__="bar"}'); + + parsed = parseSelector('baz{foo="bar"}', 12); + expect(parsed.selector).toBe('{__name__="baz",foo="bar"}'); }); }); diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index ab77271076d..f5ccb848f2f 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; const selectorRegexp = /\{[^}]*?\}/; const labelRegexp = /\b\w+="[^"\n]*?"/g; -export function getCleanSelector(query: string, cursorOffset = 1): string { +export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics if (query.match(/^\w+$/)) { - return `{__name__="${query}"}`; + return { + selector: `{__name__="${query}"}`, + labelKeys: ['__name__'], + }; } throw new Error('Query must contain a selector: ' + query); } @@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string { } // Build sorted selector - const cleanSelector = Object.keys(labels) - .sort() - .map(key => `${key}=${labels[key]}`) - .join(','); + const labelKeys = Object.keys(labels).sort(); + const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(','); - return ['{', cleanSelector, '}'].join(''); + const selectorString = ['{', cleanSelector, '}'].join(''); + + return { labelKeys, selector: selectorString }; } From 0f5945c5578b3a4e2d469a4d2fb0bf3efde2db09 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 15:29:21 +0200 Subject: [PATCH 03/14] Explore: still show rate hint if query is complex - action hint currently only works for very simple queries - show a hint w/o action otherwise --- .../datasource/prometheus/datasource.ts | 24 ++++++++++++------- .../prometheus/specs/datasource.jest.ts | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ef440ab515d..208a7b6a2f0 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -110,10 +110,9 @@ export function determineQueryHints(series: any[], datasource?: any): any[] { // Check for monotony const datapoints: [number, number][] = s.datapoints; - const simpleMetric = query.trim().match(/^\w+$/); - if (simpleMetric && datapoints.length > 1) { + if (datapoints.length > 1) { let increasing = false; - const monotonic = datapoints.every((dp, index) => { + const monotonic = datapoints.filter(dp => dp[0] !== null).every((dp, index) => { if (index === 0) { return true; } @@ -122,18 +121,25 @@ export function determineQueryHints(series: any[], datasource?: any): any[] { return dp[0] >= datapoints[index - 1][0]; }); if (increasing && monotonic) { - const label = 'Time series is monotonously increasing.'; - return { - label, - index, - fix: { + const simpleMetric = query.trim().match(/^\w+$/); + let label = 'Time series is monotonously increasing.'; + let fix; + if (simpleMetric) { + fix = { label: 'Fix by adding rate().', action: { type: 'ADD_RATE', query, index, }, - }, + }; + } else { + label = `${label} Try applying a rate() function.`; + } + return { + label, + index, + fix, }; } } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index a108909e6e1..fea60658332 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -213,6 +213,30 @@ describe('PrometheusDatasource', () => { }); }); + it('returns a rate hint w/o action for a complex monotonously increasing series', () => { + const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }]; + const hints = determineQueryHints(series); + expect(hints.length).toBe(1); + expect(hints[0].label).toContain('rate()'); + expect(hints[0].fix).toBeUndefined(); + }); + + it('returns a rate hint for a monotonously increasing series with missing data', () => { + const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]], query: 'metric', responseIndex: 0 }]; + const hints = determineQueryHints(series); + expect(hints.length).toBe(1); + expect(hints[0]).toMatchObject({ + label: 'Time series is monotonously increasing.', + index: 0, + fix: { + action: { + type: 'ADD_RATE', + query: 'metric', + }, + }, + }); + }); + it('returns a histogram hint for a bucket series', () => { const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }]; const hints = determineQueryHints(series); From 978e89657ecd4f8795721db2b9c21ea2ab1a0655 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 13 Aug 2018 12:53:12 +0200 Subject: [PATCH 04/14] Explore: Fix label filtering for rate queries - exclude `]` from match expression for selector injection to ignore range vectors like `[10m]` --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../app/plugins/datasource/prometheus/specs/datasource.jest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 318b0f8f1fc..9d4d0433d5d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri // Add empty selector to bare metric name let previousWord; - query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => { + query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => { // Check if inside a selector const nextSelectorStart = query.slice(offset).indexOf('{'); const nextSelectorEnd = query.slice(offset).indexOf('}'); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index 4ba2e3260a7..ed467c54b24 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -351,6 +351,7 @@ describe('PrometheusDatasource', () => { expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( 'foo{bar="baz",instance="my-host.com:9100"}' ); + expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); }); }); From d6ad1ced6d88ee944e2e04d33a6d3405a54ae5df Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 17 Aug 2018 12:20:21 +0200 Subject: [PATCH 05/14] when value in variable changes, identify which variable(s) to update Given you have variables a, b, c, d where b depends on a, c depends on b, c, d depends on a. When updating a only an update of b and d should be triggered since c depends on b and c will be updated eventually when the update of b are finished. --- public/app/core/utils/dag.test.ts | 108 ++++++++++ public/app/core/utils/dag.ts | 201 ++++++++++++++++++ .../app/features/templating/variable_srv.ts | 38 +++- 3 files changed, 337 insertions(+), 10 deletions(-) create mode 100644 public/app/core/utils/dag.test.ts create mode 100644 public/app/core/utils/dag.ts diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts new file mode 100644 index 00000000000..a89ab27cda3 --- /dev/null +++ b/public/app/core/utils/dag.test.ts @@ -0,0 +1,108 @@ +import { Graph } from './dag'; + +describe('Directed acyclic graph', () => { + describe('Given a graph with nodes with different links in between them', () => { + let dag = new Graph(); + let nodeA = dag.createNode('A'); + let nodeB = dag.createNode('B'); + let nodeC = dag.createNode('C'); + let nodeD = dag.createNode('D'); + let nodeE = dag.createNode('E'); + let nodeF = dag.createNode('F'); + let nodeG = dag.createNode('G'); + let nodeH = dag.createNode('H'); + let nodeI = dag.createNode('I'); + dag.link([nodeB, nodeC, nodeD, nodeE, nodeF, nodeG, nodeH], nodeA); + dag.link([nodeC, nodeD, nodeE, nodeF, nodeI], nodeB); + dag.link([nodeD, nodeE, nodeF, nodeG], nodeC); + dag.link([nodeE, nodeF], nodeD); + dag.link([nodeF, nodeG], nodeE); + //printGraph(dag); + + it('nodes in graph should have expected edges', () => { + expect(nodeA.inputEdges).toHaveLength(7); + expect(nodeA.outputEdges).toHaveLength(0); + expect(nodeA.edges).toHaveLength(7); + + expect(nodeB.inputEdges).toHaveLength(5); + expect(nodeB.outputEdges).toHaveLength(1); + expect(nodeB.edges).toHaveLength(6); + + expect(nodeC.inputEdges).toHaveLength(4); + expect(nodeC.outputEdges).toHaveLength(2); + expect(nodeC.edges).toHaveLength(6); + + expect(nodeD.inputEdges).toHaveLength(2); + expect(nodeD.outputEdges).toHaveLength(3); + expect(nodeD.edges).toHaveLength(5); + + expect(nodeE.inputEdges).toHaveLength(2); + expect(nodeE.outputEdges).toHaveLength(4); + expect(nodeE.edges).toHaveLength(6); + + expect(nodeF.inputEdges).toHaveLength(0); + expect(nodeF.outputEdges).toHaveLength(5); + expect(nodeF.edges).toHaveLength(5); + + expect(nodeG.inputEdges).toHaveLength(0); + expect(nodeG.outputEdges).toHaveLength(3); + expect(nodeG.edges).toHaveLength(3); + + expect(nodeH.inputEdges).toHaveLength(0); + expect(nodeH.outputEdges).toHaveLength(1); + expect(nodeH.edges).toHaveLength(1); + + expect(nodeI.inputEdges).toHaveLength(0); + expect(nodeI.outputEdges).toHaveLength(1); + expect(nodeI.edges).toHaveLength(1); + + expect(nodeA.getEdgeFrom(nodeB)).not.toBeUndefined(); + expect(nodeB.getEdgeTo(nodeA)).not.toBeUndefined(); + }); + + it('when optimizing input edges for node A should return node B and H', () => { + const actual = nodeA.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeB, nodeH])); + }); + + it('when optimizing input edges for node B should return node C', () => { + const actual = nodeB.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeC, nodeI])); + }); + + it('when optimizing input edges for node C should return node D', () => { + const actual = nodeC.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(1); + expect(actual).toEqual(expect.arrayContaining([nodeD])); + }); + + it('when optimizing input edges for node D should return node E', () => { + const actual = nodeD.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(1); + expect(actual).toEqual(expect.arrayContaining([nodeE])); + }); + + it('when optimizing input edges for node E should return node F and G', () => { + const actual = nodeE.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeF, nodeG])); + }); + + it('when optimizing input edges for node F should return zero nodes', () => { + const actual = nodeF.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + + it('when optimizing input edges for node G should return zero nodes', () => { + const actual = nodeG.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + + it('when optimizing input edges for node H should return zero nodes', () => { + const actual = nodeH.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + }); +}); diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts new file mode 100644 index 00000000000..1d61280fb05 --- /dev/null +++ b/public/app/core/utils/dag.ts @@ -0,0 +1,201 @@ +export class Edge { + inputNode: Node; + outputNode: Node; + + _linkTo(node, direction) { + if (direction <= 0) { + node.inputEdges.push(this); + } + + if (direction >= 0) { + node.outputEdges.push(this); + } + + node.edges.push(this); + } + + link(inputNode: Node, outputNode: Node) { + this.unlink(); + this.inputNode = inputNode; + this.outputNode = outputNode; + + this._linkTo(inputNode, 1); + this._linkTo(outputNode, -1); + return this; + } + + unlink() { + let pos; + let inode = this.inputNode; + let onode = this.outputNode; + + if (!(inode && onode)) { + return; + } + + pos = inode.edges.indexOf(this); + if (pos > -1) { + inode.edges.splice(pos, 1); + } + + pos = onode.edges.indexOf(this); + if (pos > -1) { + onode.edges.splice(pos, 1); + } + + pos = inode.outputEdges.indexOf(this); + if (pos > -1) { + inode.outputEdges.splice(pos, 1); + } + + pos = onode.inputEdges.indexOf(this); + if (pos > -1) { + onode.inputEdges.splice(pos, 1); + } + + this.inputNode = null; + this.outputNode = null; + } +} + +export class Node { + name: string; + edges: Edge[]; + inputEdges: Edge[]; + outputEdges: Edge[]; + + constructor(name: string) { + this.name = name; + this.edges = []; + this.inputEdges = []; + this.outputEdges = []; + } + + getEdgeFrom(from: string | Node): Edge { + if (!from) { + return null; + } + + if (typeof from === 'object') { + return this.inputEdges.find(e => e.inputNode.name === from.name); + } + + return this.inputEdges.find(e => e.inputNode.name === from); + } + + getEdgeTo(to: string | Node): Edge { + if (!to) { + return null; + } + + if (typeof to === 'object') { + return this.outputEdges.find(e => e.outputNode.name === to.name); + } + + return this.outputEdges.find(e => e.outputNode.name === to); + } + + getOptimizedInputEdges(): Edge[] { + let toBeRemoved = []; + this.inputEdges.forEach(e => { + let inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); + + inputEdgesNodes.forEach(n => { + let edgeToRemove = n.getEdgeTo(this.name); + if (edgeToRemove) { + toBeRemoved.push(edgeToRemove); + } + }); + }); + + return this.inputEdges.filter(e => toBeRemoved.indexOf(e) === -1); + } +} + +export class Graph { + nodes = {}; + + constructor() {} + + createNode(name: string): Node { + const n = new Node(name); + this.nodes[name] = n; + return n; + } + + createNodes(names: string[]): Node[] { + let nodes = []; + names.forEach(name => { + nodes.push(this.createNode(name)); + }); + return nodes; + } + + link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] { + let inputArr = []; + let outputArr = []; + let inputNodes = []; + let outputNodes = []; + + if (input instanceof Array) { + inputArr = input; + } else { + inputArr = [input]; + } + + if (output instanceof Array) { + outputArr = output; + } else { + outputArr = [output]; + } + + for (let n = 0; n < inputArr.length; n++) { + const i = inputArr[n]; + if (typeof i === 'string') { + inputNodes.push(this.getNode(i)); + } else { + inputNodes.push(i); + } + } + + for (let n = 0; n < outputArr.length; n++) { + const i = outputArr[n]; + if (typeof i === 'string') { + outputNodes.push(this.getNode(i)); + } else { + outputNodes.push(i); + } + } + + let edges = []; + inputNodes.forEach(input => { + outputNodes.forEach(output => { + edges.push(this.createEdge().link(input, output)); + }); + }); + return edges; + } + + createEdge(): Edge { + return new Edge(); + } + + getNode(name: string): Node { + return this.nodes[name]; + } +} + +export const printGraph = (g: Graph) => { + Object.keys(g.nodes).forEach(name => { + const n = g.nodes[name]; + let outputEdges = n.outputEdges.map(e => e.outputNode.name).join(', '); + if (!outputEdges) { + outputEdges = ''; + } + let inputEdges = n.inputEdges.map(e => e.inputNode.name).join(', '); + if (!inputEdges) { + inputEdges = ''; + } + console.log(`${n.name}:\n - links to: ${outputEdges}\n - links from: ${inputEdges}`); + }); +}; diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 8ad3c2845e2..bd214639552 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; import { variableTypes } from './variable'; +import { Graph } from 'app/core/utils/dag'; export class VariableSrv { dashboard: any; @@ -120,16 +121,13 @@ export class VariableSrv { return this.$q.when(); } - // cascade updates to variables that use this variable - var promises = _.map(this.variables, otherVariable => { - if (otherVariable === variable) { - return; - } - - if (otherVariable.dependsOn(variable)) { - return this.updateOptions(otherVariable); - } - }); + const g = this.createGraph(); + const promises = g + .getNode(variable.name) + .getOptimizedInputEdges() + .map(e => { + return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name)); + }); return this.$q.all(promises).then(() => { if (emitChangeEvents) { @@ -288,6 +286,26 @@ export class VariableSrv { filter.operator = options.operator; this.variableUpdated(variable, true); } + + createGraph() { + let g = new Graph(); + + this.variables.forEach(v1 => { + g.createNode(v1.name); + + this.variables.forEach(v2 => { + if (v1 === v2) { + return; + } + + if (v1.dependsOn(v2)) { + g.link(v1.name, v2.name); + } + }); + }); + + return g; + } } coreModule.service('variableSrv', VariableSrv); From c75e07121381a31cc0504c5a69bbaa468ac212ab Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sat, 18 Aug 2018 16:00:40 +0200 Subject: [PATCH 06/14] dsproxy: interpolate route url Allows for dynamic urls for plugin routes. There are a few plugins where the route url should be configurable and this change allows using jsonData fields in the url field for a route in the plugin.json file for a plugin. --- pkg/api/pluginproxy/ds_proxy.go | 10 ++++++++-- pkg/api/pluginproxy/ds_proxy_test.go | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index c8056040d24..fb2cab9b9b1 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -320,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) { SecureJsonData: proxy.ds.SecureJsonData.Decrypt(), } - routeURL, err := url.Parse(proxy.route.Url) + interpolatedURL, err := interpolateString(proxy.route.Url, data) if err != nil { - logger.Error("Error parsing plugin route url") + logger.Error("Error interpolating proxy url", "error", err) + return + } + + routeURL, err := url.Parse(interpolatedURL) + if err != nil { + logger.Error("Error parsing plugin route url", "error", err) return } diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index ad331113f46..e6d05872787 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) { {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, }, }, + { + Path: "api/common", + Url: "{{.JsonData.dynamicUrl}}", + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, + }, }, } @@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) { ds := &m.DataSource{ JsonData: simplejson.NewFromAny(map[string]interface{}{ - "clientId": "asd", + "clientId": "asd", + "dynamicUrl": "https://dynamic.grafana.com", }), SecureJsonData: map[string][]byte{ "key": key, @@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) { }) }) + Convey("When matching route path and has dynamic url", func() { + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method") + proxy.route = plugin.Routes[3] + proxy.applyRoute(req) + + Convey("should add headers and interpolate the url", func() { + So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method") + So(req.Header.Get("x-header"), ShouldEqual, "my secret 123") + }) + }) + Convey("Validating request", func() { Convey("plugin route with valid role", func() { proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method") From a92d51731d36b818713b0d0004be8f598ba962af Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Mon, 20 Aug 2018 11:55:29 +0200 Subject: [PATCH 07/14] Webpack tapable plugin deprecation (#12960) * Remove unrequired extract-text-webpack-plugin * Update ngAnnotate to avoid deprecation warning * Avoid deprecation warning (Tapable.plugin -> hooks) --- package.json | 3 +-- scripts/webpack/webpack.dev.js | 1 - scripts/webpack/webpack.prod.js | 2 +- yarn.lock | 15 +++------------ 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 87615e8273b..8520def5db8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "es6-shim": "^0.35.3", "expect.js": "~0.2.0", "expose-loader": "^0.7.3", - "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", "fork-ts-checker-webpack-plugin": "^0.4.2", "gaze": "^1.1.2", @@ -63,7 +62,7 @@ "mobx-react-devtools": "^4.2.15", "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", - "ng-annotate-webpack-plugin": "^0.2.1-pre", + "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 1e54ca73a19..7eecceeb1bf 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -5,7 +5,6 @@ const common = require('./webpack.common.js'); const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require("html-webpack-plugin"); -const ExtractTextPlugin = require("extract-text-webpack-plugin"); const CleanWebpackPlugin = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index 9c0b0cd093c..9e1e4cfb0b5 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -81,7 +81,7 @@ module.exports = merge(common, { chunks: ['vendor', 'app'], }), function () { - this.plugin("done", function (stats) { + this.hooks.done.tap('Done', function (stats) { if (stats.compilation.errors && stats.compilation.errors.length) { console.log(stats.compilation.errors); process.exit(1); diff --git a/yarn.lock b/yarn.lock index c4bd6704839..dd1cde4e698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4105,15 +4105,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-text-webpack-plugin@^4.0.0-beta.0: - version "4.0.0-beta.0" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-4.0.0-beta.0.tgz#f7361d7ff430b42961f8d1321ba8c1757b5d4c42" - dependencies: - async "^2.4.1" - loader-utils "^1.1.0" - schema-utils "^0.4.5" - webpack-sources "^1.1.0" - extract-zip@^1.6.5: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" @@ -7661,9 +7652,9 @@ ng-annotate-loader@^0.6.1: normalize-path "2.0.1" source-map "0.5.6" -ng-annotate-webpack-plugin@^0.2.1-pre: - version "0.2.1-pre" - resolved "https://registry.yarnpkg.com/ng-annotate-webpack-plugin/-/ng-annotate-webpack-plugin-0.2.1-pre.tgz#40d9aa8cd214e30e3125a8481634ab0dd9b3dd68" +ng-annotate-webpack-plugin@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ng-annotate-webpack-plugin/-/ng-annotate-webpack-plugin-0.3.0.tgz#2e7f5e29c6a4ce26649edcb06c1213408b35b84a" dependencies: ng-annotate "^1.2.1" webpack-core "^0.6.5" From 0223a75de00f23c45fe4140b995f285570d62671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= Date: Mon, 20 Aug 2018 06:56:12 -0300 Subject: [PATCH 08/14] Refresh query variable when another variable is used in regex field (#12961) --- public/app/features/templating/query_variable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 5ddd6d32864..827fd80a176 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -213,7 +213,7 @@ export class QueryVariable implements Variable { } dependsOn(variable) { - return containsVariable(this.query, this.datasource, variable.name); + return containsVariable(this.query, this.datasource, this.regex, variable.name); } } From cf632c0f11b8b3a18abb4c2e8eacb0b79794728f Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Mon, 20 Aug 2018 19:21:32 +0200 Subject: [PATCH 09/14] Fix bulk-dashboards path (#12978) --- .gitignore | 2 +- devenv/bulk-dashboards/bulk-dashboards.yaml | 2 +- devenv/setup.sh | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2484176a469..bf97948d178 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,4 @@ debug.test /vendor/**/appengine* *.orig -/devenv/dashboards/bulk-testing/*.json +/devenv/bulk-dashboards/*.json diff --git a/devenv/bulk-dashboards/bulk-dashboards.yaml b/devenv/bulk-dashboards/bulk-dashboards.yaml index e0ba8a88e68..65557901f42 100644 --- a/devenv/bulk-dashboards/bulk-dashboards.yaml +++ b/devenv/bulk-dashboards/bulk-dashboards.yaml @@ -5,5 +5,5 @@ providers: folder: 'Bulk dashboards' type: file options: - path: devenv/dashboards/bulk-testing + path: devenv/bulk-dashboards diff --git a/devenv/setup.sh b/devenv/setup.sh index 6412bbc98ea..cc71ecc71bf 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -7,11 +7,11 @@ bulkDashboard() { COUNTER=0 MAX=400 while [ $COUNTER -lt $MAX ]; do - jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" + jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" let COUNTER=COUNTER+1 done - ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml + ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml } requiresJsonnet() { From 72efd73c3baaf7d8721309ad9002dc749d81a306 Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Mon, 20 Aug 2018 19:22:30 +0200 Subject: [PATCH 10/14] Show min-width option only for horizontal repeat (#12981) --- public/app/partials/panelgeneral.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index eb17d743d33..797c252331c 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -18,18 +18,18 @@ For each value of -
- Min width - -
Direction
+
+ Min width + +
From 6316d637f1c68fcad3f9ccdf62f6b7d8371fffc8 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 20 Aug 2018 19:22:55 +0200 Subject: [PATCH 11/14] Explore: Apply tab completion suggestion on Enter (#12904) - if the suggestions menu is open, apply the selected item on Enter - if not open, run the queries --- public/app/containers/Explore/QueryField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 04481885a1c..52bfbc7fed4 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -331,7 +331,7 @@ class QueryField extends React.Component Date: Tue, 21 Aug 2018 10:51:38 +0200 Subject: [PATCH 12/14] changelog: add notes about closing #11890 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98efa2b1099..651b4dd22f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890) * **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) From 92ed1f04afcdead02fe3a8bf53caecc89db1c5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 21 Aug 2018 13:30:39 +0200 Subject: [PATCH 13/14] sql: added code migration type --- pkg/api/login.go | 8 +++- pkg/services/sqlstore/migrations/user_mig.go | 41 +++++++++++++++++++- pkg/services/sqlstore/migrator/migrator.go | 16 +++++--- pkg/services/sqlstore/migrator/types.go | 7 ++++ pkg/services/sqlstore/user.go | 5 ++- pkg/services/sqlstore/user_test.go | 22 +++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index 01fa71a6e44..632d04e37f1 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { user := userQuery.Result // validate remember me cookie - if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login { + signingKey := user.Rands + user.Password + if len(signingKey) < 10 { + c.Logger.Error("Invalid user signingKey") + return false + } + + if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login { return false } diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index edcfbb7b889..400033aaa33 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -1,6 +1,12 @@ package migrations -import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +import ( + "fmt" + + "github.com/go-xorm/xorm" + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util" +) func addUserMigrations(mg *Migrator) { userV1 := Table{ @@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{ Name: "last_seen_at", Type: DB_DateTime, Nullable: true, })) + + // Adds salt & rands for old users who used ldap or oauth + mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{}) +} + +type AddMissingUserSaltAndRandsMigration struct { + MigrationBase +} + +func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string { + return "code migration" +} + +type TempUserDTO struct { + Id int64 + Login string +} + +func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error { + users := make([]*TempUserDTO, 0) + + err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users) + if err != nil { + return err + } + + for _, user := range users { + _, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id) + if err != nil { + return err + } + } + return nil } diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 9bdaaf7cc14..dead6f2b416 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -12,7 +12,7 @@ import ( type Migrator struct { x *xorm.Engine - dialect Dialect + Dialect Dialect migrations []Migration Logger log.Logger } @@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator { mg.x = engine mg.Logger = log.New("migrator") mg.migrations = make([]Migration, 0) - mg.dialect = NewDialect(mg.x) + mg.Dialect = NewDialect(mg.x) return mg } @@ -86,7 +86,7 @@ func (mg *Migrator) Start() error { continue } - sql := m.Sql(mg.dialect) + sql := m.Sql(mg.Dialect) record := MigrationLog{ MigrationId: m.Id(), @@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { - sql, args := condition.Sql(mg.dialect) + sql, args := condition.Sql(mg.Dialect) results, err := sess.SQL(sql).Query(args...) if err != nil || len(results) == 0 { mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id()) @@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { } } - _, err := sess.Exec(m.Sql(mg.dialect)) + var err error + if codeMigration, ok := m.(CodeMigration); ok { + err = codeMigration.Exec(sess, mg) + } else { + _, err = sess.Exec(m.Sql(mg.Dialect)) + } + if err != nil { mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err) return err diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go index 26c46889daf..48354998d8d 100644 --- a/pkg/services/sqlstore/migrator/types.go +++ b/pkg/services/sqlstore/migrator/types.go @@ -3,6 +3,8 @@ package migrator import ( "fmt" "strings" + + "github.com/go-xorm/xorm" ) const ( @@ -19,6 +21,11 @@ type Migration interface { GetCondition() MigrationCondition } +type CodeMigration interface { + Migration + Exec(sess *xorm.Session, migrator *Migrator) error +} + type SQLType string type ColumnType string diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 0ec1a947870..5d1b827e79f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -113,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error { LastSeenAt: time.Now().AddDate(-10, 0, 0), } + user.Salt = util.GetRandomString(10) + user.Rands = util.GetRandomString(10) + if len(cmd.Password) > 0 { - user.Salt = util.GetRandomString(10) - user.Rands = util.GetRandomString(10) user.Password = util.EncodePassword(cmd.Password, user.Salt) } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index a76ae860b7d..b26dd235772 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) { Convey("Testing DB", t, func() { InitTestDB(t) + Convey("Creating a user", func() { + cmd := &m.CreateUserCommand{ + Email: "usertest@test.com", + Name: "user name", + Login: "user_test_login", + } + + err := CreateUser(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("Loading a user", func() { + query := m.GetUserByIdQuery{Id: cmd.Result.Id} + err := GetUserById(&query) + So(err, ShouldBeNil) + + So(query.Result.Email, ShouldEqual, "usertest@test.com") + So(query.Result.Password, ShouldEqual, "") + So(query.Result.Rands, ShouldHaveLength, 10) + So(query.Result.Salt, ShouldHaveLength, 10) + }) + }) + Convey("Given 5 users", func() { var err error var cmd *m.CreateUserCommand From 2b1f84cd43c0d9175d7c9bd353629f77d39b7ad3 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 21 Aug 2018 15:53:57 +0200 Subject: [PATCH 14/14] Update notifications.md --- docs/sources/alerting/notifications.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index b3b4305a748..58046cafae4 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -130,7 +130,7 @@ There are a couple of configuration options which need to be set up in Grafana U Once these two properties are set, you can send the alerts to Kafka for further processing or throttling. -### All supported notifier +### All supported notifiers Name | Type |Support images -----|------------ | ------ @@ -148,6 +148,7 @@ Pushover | `pushover` | no Telegram | `telegram` | no Line | `line` | no Prometheus Alertmanager | `prometheus-alertmanager` | no +Microsoft Teams | `teams` | yes