From e62c083cf0da76d995ef71e51b5b1e68364ef6e2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Jun 2018 14:03:52 +0900 Subject: [PATCH 1/6] use series matchers to get label name/value --- .../datasource/prometheus/completer.ts | 18 +++++++++------- .../datasource/prometheus/datasource.ts | 8 +++++++ .../prometheus/specs/completer.test.ts | 21 ++++--------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 396a5fc1cd7..3cf4505e16a 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -113,7 +113,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -151,7 +151,7 @@ export class PromCompleter { var labelValues = this.transformToCompletions( _.uniq( result.map(r => { - return r.metric[labelName]; + return r[labelName]; }) ), 'label value' @@ -191,7 +191,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -233,7 +233,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -249,7 +249,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -276,9 +276,11 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => { - this.labelQueryCache[expr] = response.data.data.result; - return response.data.data.result; + let range = this.datasource.getTimeRange(); + let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + return this.datasource.metadataRequest(url).then(response => { + this.labelQueryCache[expr] = response.data.data; + return response.data.data; }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 057bb55b3c3..c019fdc4aab 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,6 +629,14 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } + getTimeRange() { + let range = this.timeSrv.timeRange(); + return { + from: this.getPrometheusTime(range.from, false), + to: this.getPrometheusTime(range.to, true) + }; + } + getOriginalMetricName(labelData) { return this.resultTransformer.getOriginalMetricName(labelData); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 59fcc6592fb..201c8fcb0d7 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function() { +describe('Prometheus editor completer', function () { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -18,22 +18,9 @@ describe('Prometheus editor completer', function() { const backendSrv = {}; const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); - datasourceStub.performInstantQuery = jest.fn(() => - Promise.resolve({ - data: { - data: { - result: [ - { - metric: { - job: 'node', - instance: 'localhost:9100', - }, - }, - ], - }, - }, - }) - ); + datasourceStub.metadataRequest = jest.fn(() => + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); + datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From bf8840255c1e1236ddddfbcf00318e7a85229689 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Jul 2018 11:39:00 +0900 Subject: [PATCH 2/6] Review feedback. --- public/app/plugins/datasource/prometheus/completer.ts | 6 +++--- public/app/plugins/datasource/prometheus/datasource.ts | 6 +++--- .../datasource/prometheus/specs/completer.test.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 3cf4505e16a..5719eeb5ac3 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -264,7 +264,7 @@ export class PromCompleter { return Promise.resolve([]); } - getLabelNameAndValueForExpression(expr, type) { + getLabelNameAndValueForExpression(expr: string, type: string): Promise { if (this.labelQueryCache[expr]) { return Promise.resolve(this.labelQueryCache[expr]); } @@ -276,8 +276,8 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - let range = this.datasource.getTimeRange(); - let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + const { start, end } = this.datasource.getTimeRange(); + const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; return this.datasource.metadataRequest(url).then(response => { this.labelQueryCache[expr] = response.data.data; return response.data.data; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c019fdc4aab..7f4b2fb1c98 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,11 +629,11 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } - getTimeRange() { + getTimeRange(): { start: number; end: number } { let range = this.timeSrv.timeRange(); return { - from: this.getPrometheusTime(range.from, false), - to: this.getPrometheusTime(range.to, true) + start: this.getPrometheusTime(range.from, false), + end: this.getPrometheusTime(range.to, true), }; } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 201c8fcb0d7..7a616c80c74 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function () { +describe('Prometheus editor completer', function() { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -19,8 +19,11 @@ describe('Prometheus editor completer', function () { const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.metadataRequest = jest.fn(() => - Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); - datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } }) + ); + datasourceStub.getTimeRange = jest.fn(() => { + return { start: 1514732400, end: 1514818800 }; + }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From c7bb44b34a73a8a5f82d8cc365ace7a0dd8f450f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 13:13:47 +0200 Subject: [PATCH 3/6] fix: url update loop fix (#13243) --- public/app/core/specs/url.test.ts | 16 ++++++++++++++++ public/app/core/utils/url.ts | 4 +--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 public/app/core/specs/url.test.ts diff --git a/public/app/core/specs/url.test.ts b/public/app/core/specs/url.test.ts new file mode 100644 index 00000000000..b5994488128 --- /dev/null +++ b/public/app/core/specs/url.test.ts @@ -0,0 +1,16 @@ +import { toUrlParams } from '../utils/url'; + +describe('toUrlParams', () => { + it('should encode object properties as url parameters', () => { + const url = toUrlParams({ + server: 'backend-01', + hasSpace: 'has space', + many: ['1', '2', '3'], + true: true, + number: 20, + isNull: null, + isUndefined: undefined, + }); + expect(url).toBe('server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined='); + }); +}); diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index 04c3e9a4308..198029b0e9f 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -50,7 +50,5 @@ export function toUrlParams(a) { return s; }; - return buildParams('', a) - .join('&') - .replace(/%20/g, '+'); + return buildParams('', a).join('&'); } From a5bcd4b8e42bc35cb29653768b1331c8b86a793d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 12 Sep 2018 18:10:57 +0200 Subject: [PATCH 4/6] Adhoc-filtering for prometheus dashboards (#13212) * Basic adhoc-filtering support for prometheus --- .../app/features/dashboard/ad_hoc_filters.ts | 6 +- .../app/features/templating/template_srv.ts | 7 +- .../prometheus/add_label_to_query.ts | 93 +++++++++++++++++++ .../datasource/prometheus/datasource.ts | 85 ++++------------- .../specs/add_label_to_query.test.ts | 42 +++++++++ .../prometheus/specs/datasource.test.ts | 63 ++++++++----- 6 files changed, 196 insertions(+), 100 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/add_label_to_query.ts create mode 100644 public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 68b068152b5..0ceac9ddbba 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -59,10 +59,10 @@ export class AdHocFiltersCtrl { let promise = null; if (segment.type !== 'value') { - promise = ds.getTagKeys(); + promise = ds.getTagKeys ? ds.getTagKeys() : Promise.resolve([]); } else { options.key = this.segments[index - 2].value; - promise = ds.getTagValues(options); + promise = ds.getTagValues ? ds.getTagValues(options) : Promise.resolve([]); } return promise.then(results => { @@ -99,7 +99,7 @@ export class AdHocFiltersCtrl { this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); } this.segments.push(this.uiSegmentSrv.newOperator('=')); - this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value')); + this.segments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value')); segment.type = 'key'; segment.cssClass = 'query-segment-key'; } diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 6eab51abbfa..def9fda1f56 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -56,11 +56,10 @@ export class TemplateSrv { continue; } - if (variable.datasource === datasourceName) { + // null is the "default" datasource + if (variable.datasource === null || variable.datasource === datasourceName) { filters = filters.concat(variable.filters); - } - - if (variable.datasource.indexOf('$') === 0) { + } else if (variable.datasource.indexOf('$') === 0) { if (this.replace(variable.datasource) === datasourceName) { filters = filters.concat(variable.filters); } diff --git a/public/app/plugins/datasource/prometheus/add_label_to_query.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.ts new file mode 100644 index 00000000000..9ea01ed755a --- /dev/null +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; + +const keywords = 'by|without|on|ignoring|group_left|group_right'; + +// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded. +const builtInWords = [ + keywords, + 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile', + 'true|false|null|__name__|job', + 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv', + 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2', + 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time', + 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time', +] + .join('|') + .split('|'); + +const metricNameRegexp = /([A-Za-z]\w*)\b(?![\(\]{=!",])/g; +const selectorRegexp = /{([^{]*)}/g; + +// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' +export function addLabelToQuery(query: string, key: string, value: string, operator?: string): string { + if (!key || !value) { + throw new Error('Need label to add to query.'); + } + + // Add empty selectors to bare metric names + let previousWord; + query = query.replace(metricNameRegexp, (match, word, offset) => { + const insideSelector = isPositionInsideChars(query, offset, '{', '}'); + // Handle "sum by (key) (metric)" + const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1; + previousWord = word; + if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) { + return `${word}{}`; + } + return word; + }); + + // Adding label to existing selectors + let match = selectorRegexp.exec(query); + const parts = []; + let lastIndex = 0; + let suffix = ''; + + while (match) { + const prefix = query.slice(lastIndex, match.index); + const selector = match[1]; + const selectorWithLabel = addLabelToSelector(selector, key, value, operator); + lastIndex = match.index + match[1].length + 2; + suffix = query.slice(match.index + match[0].length); + parts.push(prefix, '{', selectorWithLabel, '}'); + match = selectorRegexp.exec(query); + } + + parts.push(suffix); + return parts.join(''); +} + +const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g; + +function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) { + const parsedLabels = []; + + // Split selector into labels + if (selector) { + let match = labelRegexp.exec(selector); + while (match) { + parsedLabels.push({ key: match[1], operator: match[2], value: match[3] }); + match = labelRegexp.exec(selector); + } + } + + // Add new label + const operatorForLabelKey = labelOperator || '='; + parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` }); + + // Sort labels by key and put them together + return _.chain(parsedLabels) + .compact() + .sortBy('key') + .map(({ key, operator, value }) => `${key}${operator}${value}`) + .value() + .join(','); +} + +function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) { + const nextSelectorStart = text.slice(position).indexOf(openChar); + const nextSelectorEnd = text.slice(position).indexOf(closeChar); + return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd); +} + +export default addLabelToQuery; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 624e5694294..a07949490b2 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -7,6 +7,8 @@ import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; import { BackendSrv } from 'app/core/services/backend_srv'; +import addLabelToQuery from './add_label_to_query'; + export function alignRange(start, end, step) { const alignedEnd = Math.ceil(end / step) * step; const alignedStart = Math.floor(start / step) * step; @@ -16,74 +18,6 @@ export function alignRange(start, end, step) { }; } -const keywords = 'by|without|on|ignoring|group_left|group_right'; - -// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded. -const builtInWords = [ - keywords, - 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile', - 'true|false|null|__name__|job', - 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv', - 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2', - 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time', - 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time', -] - .join('|') - .split('|'); - -// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' -export function addLabelToQuery(query: string, key: string, value: string): string { - if (!key || !value) { - throw new Error('Need label to add to query.'); - } - - // Add empty selector to bare metric name - let previousWord; - query = query.replace(/([A-Za-z]\w*)\b(?![\(\]{=",])/g, (match, word, offset) => { - // Check if inside a selector - const nextSelectorStart = query.slice(offset).indexOf('{'); - const nextSelectorEnd = query.slice(offset).indexOf('}'); - const insideSelector = nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd); - // Handle "sum by (key) (metric)" - const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1; - previousWord = word; - if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) { - return `${word}{}`; - } - return word; - }); - - // Adding label to existing selectors - const selectorRegexp = /{([^{]*)}/g; - let match = selectorRegexp.exec(query); - const parts = []; - let lastIndex = 0; - let suffix = ''; - - while (match) { - const prefix = query.slice(lastIndex, match.index); - const selectorParts = match[1].split(','); - const labels = selectorParts.reduce((acc, label) => { - const labelParts = label.split('='); - if (labelParts.length === 2) { - acc[labelParts[0]] = labelParts[1]; - } - return acc; - }, {}); - labels[key] = `"${value}"`; - const selector = Object.keys(labels) - .sort() - .map(key => `${key}=${labels[key]}`) - .join(','); - lastIndex = match.index + match[1].length + 2; - suffix = query.slice(match.index + match[0].length); - parts.push(prefix, '{', selector, '}'); - match = selectorRegexp.exec(query); - } - parts.push(suffix); - return parts.join(''); -} - export function determineQueryHints(series: any[], datasource?: any): any[] { const hints = series.map((s, i) => { const query: string = s.query; @@ -406,8 +340,21 @@ export class PrometheusDatasource { } query.step = interval; + let expr = target.expr; + + // Apply adhoc filters + const adhocFilters = this.templateSrv.getAdhocFilters(this.name); + expr = adhocFilters.reduce((acc, filter) => { + const { key, operator } = filter; + let { value } = filter; + if (operator === '=~' || operator === '!~') { + value = prometheusSpecialRegexEscape(value); + } + return addLabelToQuery(acc, key, value, operator); + }, expr); + // Only replace vars in expression after having (possibly) updated interval vars - query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); + query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr); query.requestId = options.panelId + target.refId; // Align query interval with step diff --git a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts new file mode 100644 index 00000000000..9c654e8e467 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts @@ -0,0 +1,42 @@ +import addLabelToQuery from '../add_label_to_query'; + +describe('addLabelToQuery()', () => { + it('should add label to simple query', () => { + expect(() => { + addLabelToQuery('foo', '', ''); + }).toThrow(); + expect(addLabelToQuery('foo', 'bar', 'baz')).toBe('foo{bar="baz"}'); + expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); + expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); + expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); + }); + + it('should add custom operator', () => { + expect(addLabelToQuery('foo{}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz"}'); + expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz",x="yy"}'); + }); + + it('should not modify ranges', () => { + expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); + }); + + it('should detect in-order function use', () => { + expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})'); + }); + + it('should handle selectors with punctuation', () => { + expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( + 'foo{bar="baz",instance="my-host.com:9100"}' + ); + expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{bar="baz",list="a,b,c"}'); + }); + + it('should work on arithmetical expressions', () => { + expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); + expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); + expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); + expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( + 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' + ); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index f659c89c3ea..ae91e6647e0 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -8,11 +8,15 @@ import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape, - addLabelToQuery, } from '../datasource'; jest.mock('../metric_find_query'); +const DEFAULT_TEMPLATE_SRV_MOCK = { + getAdhocFilters: () => [], + replace: a => a, +}; + describe('PrometheusDatasource', () => { const ctx: any = {}; const instanceSettings = { @@ -25,9 +29,8 @@ describe('PrometheusDatasource', () => { ctx.backendSrvMock = {}; - ctx.templateSrvMock = { - replace: a => a, - }; + ctx.templateSrvMock = DEFAULT_TEMPLATE_SRV_MOCK; + ctx.timeSrvMock = { timeRange: () => { return { @@ -60,6 +63,37 @@ describe('PrometheusDatasource', () => { }); }); + describe('When using adhoc filters', () => { + const DEFAULT_QUERY_EXPRESSION = 'metric{job="foo"} - metric'; + const target = { expr: DEFAULT_QUERY_EXPRESSION }; + + afterEach(() => { + ctx.templateSrvMock.getAdhocFilters = DEFAULT_TEMPLATE_SRV_MOCK.getAdhocFilters; + }); + + it('should not modify expression with no filters', () => { + const result = ctx.ds.createQuery(target, { interval: '15s' }); + expect(result).toMatchObject({ expr: DEFAULT_QUERY_EXPRESSION }); + }); + + it('should add filters to expression', () => { + ctx.templateSrvMock.getAdhocFilters = () => [ + { + key: 'k1', + operator: '=', + value: 'v1', + }, + { + key: 'k2', + operator: '!=', + value: 'v2', + }, + ]; + const result = ctx.ds.createQuery(target, { interval: '15s' }); + expect(result).toMatchObject({ expr: 'metric{job="foo",k1="v1",k2!="v2"} - metric{k1="v1",k2!="v2"}' }); + }); + }); + describe('When performing performSuggestQuery', () => { it('should cache response', async () => { ctx.backendSrvMock.datasourceRequest.mockReturnValue( @@ -358,26 +392,6 @@ describe('PrometheusDatasource', () => { expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); }); - - describe('addLabelToQuery()', () => { - expect(() => { - addLabelToQuery('foo', '', ''); - }).toThrow(); - expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); - expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); - expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); - expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); - expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); - expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( - 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' - ); - expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})'); - 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])'); - expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); - }); }); const SECOND = 1000; @@ -399,6 +413,7 @@ const backendSrv = { } as any; const templateSrv = { + getAdhocFilters: () => [], replace: jest.fn(str => str), }; From f2edb82e797d38ec8a53ee2bffd0c044a6571f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:10:51 +0200 Subject: [PATCH 5/6] Folder pages to redux (#13235) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * wip: began folder to redux migration * team settings * actions for group sync * wip: progress on redux folder store * wip: folder to redux * wip: folder settings page to redux progress * mobx -> redux: major progress on folder migration * redux: moved folders to it's own features folder * fix: added loading nav states * fix: gofmt issues * wip: working on reducer test * fix: added reducer test --- public/app/containers/ContainerProps.ts | 14 -- .../ManageDashboards/FolderSettings.test.tsx | 84 --------- .../ManageDashboards/FolderSettings.tsx | 160 ------------------ public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 10 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ---------- .../folders}/FolderPermissions.tsx | 56 +++--- .../folders/FolderSettingsPage.test.tsx | 55 ++++++ .../features/folders/FolderSettingsPage.tsx | 105 ++++++++++++ .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++ public/app/features/folders/state/actions.ts | 67 ++++++++ public/app/features/folders/state/navModel.ts | 53 ++++++ .../features/folders/state/reducers.test.ts | 42 +++++ public/app/features/folders/state/reducers.ts | 33 ++++ public/app/features/teams/TeamPages.tsx | 8 +- public/app/features/teams/state/actions.ts | 117 +++---------- public/app/features/teams/state/navModel.ts | 67 ++++++++ public/app/routes/routes.ts | 6 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/stores/RootStore/RootStore.ts | 2 - public/app/stores/configureStore.ts | 2 + public/app/types/folder.ts | 18 ++ public/app/types/index.ts | 4 + yarn.lock | 30 +--- 26 files changed, 656 insertions(+), 578 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.test.tsx delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts rename public/app/{containers/ManageDashboards => features/folders}/FolderPermissions.tsx (60%) create mode 100644 public/app/features/folders/FolderSettingsPage.test.tsx create mode 100644 public/app/features/folders/FolderSettingsPage.tsx create mode 100644 public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap create mode 100644 public/app/features/folders/state/actions.ts create mode 100644 public/app/features/folders/state/navModel.ts create mode 100644 public/app/features/folders/state/reducers.test.ts create mode 100644 public/app/features/folders/state/reducers.ts create mode 100644 public/app/features/teams/state/navModel.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts create mode 100644 public/app/types/folder.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index ce09b992f80..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx deleted file mode 100644 index bed3d569bcc..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; - -describe('FolderSettings', () => { - let wrapper; - let page; - - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ - id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', - canSave: true, - version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, - }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); -}); diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

Loading

; - } - - return ( -
- -
-

Folder Settings

- -
-
-
- - -
-
- - -
- -
-
-
- ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 60% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 072908d2b8e..512927c24e6 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,25 +1,38 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +40,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { - return

Loading

; + if (folder.id === 0) { + return ; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
- +

Folder Permissions

@@ -77,4 +82,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx new file mode 100644 index 00000000000..3680fa9a197 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel } from 'app/types'; +import { shallow } from 'enzyme'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + url: 'url', + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { + id: 1, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: true, + version: 1, + }, + }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx new file mode 100644 index 00000000000..1eb7ccafc65 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; + +export interface Props { + navModel: NavModel; + folderUid: string; + folder: FolderState; + getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; +} + +export class FolderSettingsPage extends PureComponent { + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; + + render() { + const { navModel, folder } = this.props; + + return ( +
+ +
+

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
+
+
+ ); + } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; + +exports[`Render should render component 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts new file mode 100644 index 00000000000..5d153b2fb8a --- /dev/null +++ b/public/app/features/folders/state/actions.ts @@ -0,0 +1,67 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', + SetFolderTitle = 'SET_FOLDER_TITLE', + SaveFolder = 'SAVE_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export interface SetFolderTitleAction { + type: ActionTypes.SetFolderTitle; + payload: string; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ + type: ActionTypes.SetFolderTitle, + payload: newTitle, +}); + +export type Action = LoadFolderAction | SetFolderTitleAction; + +type ThunkResult = ThunkAction; + + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); + }; +} + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..e6ef763d019 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,53 @@ +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..ff37f13f97f --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,42 @@ +import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; +import { inititalState, folderReducer } from './reducers'; + +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + +describe('folder reducer', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); + + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); +}); diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts new file mode 100644 index 00000000000..41ae10d19e5 --- /dev/null +++ b/public/app/features/folders/state/reducers.ts @@ -0,0 +1,33 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = { + id: 0, + uid: 'loading', + title: 'loading', + url: '', + canSave: false, + hasChanged: false, + version: 0, +}; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + hasChanged: false, + }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: action.payload.trim().length > 0, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,148 +90,73 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 519008d70f5..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...foldersReducers, }); export let store; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts new file mode 100644 index 00000000000..6fbe79cce8c --- /dev/null +++ b/public/app/types/folder.ts @@ -0,0 +1,18 @@ +export interface FolderDTO { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 92bcdb32836..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; +import { FolderDTO, FolderState } from './folder'; export { Team, @@ -19,6 +20,8 @@ export { NavIndex, UrlQueryMap, UrlQueryValue, + FolderDTO, + FolderState, }; export interface StoreState { @@ -27,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From bae560717d6f1c7694b0817a997ec278c83ee14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:23:43 +0200 Subject: [PATCH 6/6] fix: fixed tslint issue introduced in recent prometheus PR merge --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 9332a73caca..5674e1353a1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -579,7 +579,7 @@ export class PrometheusDatasource { } getTimeRange(): { start: number; end: number } { - let range = this.timeSrv.timeRange(); + const range = this.timeSrv.timeRange(); return { start: this.getPrometheusTime(range.from, false), end: this.getPrometheusTime(range.to, true),