diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 30052e32d5d..53c2a6937e3 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -19,6 +19,7 @@ export default class StackdriverDatasource { primaryAggregation: t.aggregation.crossSeriesReducer, groupBys: t.aggregation.groupBys, view: t.view || 'FULL', + filters: t.filters, })); const { data } = await this.backendSrv.datasourceRequest({ diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index e2abce4e4a4..0569c3f3081 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -9,11 +9,6 @@ export interface QueryMeta { resourceLabels: { [key: string]: string[] }; } -export interface Filter { - key: string; - operator: string; - value: string; -} export class StackdriverQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; target: { @@ -29,9 +24,12 @@ export class StackdriverQueryCtrl extends QueryCtrl { perSeriesAligner: string; groupBys: string[]; }; - filters: Filter[]; + filters: string[]; }; - defaultDropdownValue = 'Select metric'; + defaultDropdownValue = 'select metric'; + defaultFilterValue = 'select value'; + defaultRemoveGroupByValue = '-- remove group by --'; + defaultRemoveFilterValue = '-- remove filter --'; defaults = { project: { @@ -96,10 +94,21 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.ensurePlusButton(this.groupBySegments); this.filterSegments = []; - this.target.filters.forEach(f => { - this.filterSegments.push(this.uiSegmentSrv.newKey(f.key)); - this.filterSegments.push(this.uiSegmentSrv.newOperator(f.operator)); - this.filterSegments.push(this.uiSegmentSrv.newKeyValue(f.value)); + this.target.filters.forEach((f, index) => { + switch (index % 4) { + case 0: + this.filterSegments.push(this.uiSegmentSrv.newKey(f)); + break; + case 1: + this.filterSegments.push(this.uiSegmentSrv.newOperator(f)); + break; + case 2: + this.filterSegments.push(this.uiSegmentSrv.newKeyValue(f)); + break; + case 3: + this.filterSegments.push(this.uiSegmentSrv.newCondition(f)); + break; + } }); this.ensurePlusButton(this.filterSegments); } @@ -169,9 +178,12 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.getLabels(); } - getGroupBys(segment, index, removeText?: string) { + getGroupBys(segment, index, removeText?: string, removeUsed = true) { const metricLabels = Object.keys(this.metricLabels) .filter(ml => { + if (!removeUsed) { + return true; + } return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1; }) .map(l => { @@ -183,6 +195,10 @@ export class StackdriverQueryCtrl extends QueryCtrl { const resourceLabels = Object.keys(this.resourceLabels) .filter(ml => { + if (!removeUsed) { + return true; + } + return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1; }) .map(l => { @@ -192,7 +208,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { }); }); - this.removeSegment.value = removeText || '-- remove group by --'; + this.removeSegment.value = removeText || this.defaultRemoveGroupByValue; return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]); } @@ -215,7 +231,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.refresh(); } - getFilters(segment, index) { + async getFilters(segment, index) { if (segment.type === 'condition') { return [this.uiSegmentSrv.newSegment('AND')]; } @@ -225,18 +241,19 @@ export class StackdriverQueryCtrl extends QueryCtrl { } if (segment.type === 'key' || segment.type === 'plus-button') { - return this.getGroupBys(null, null, '-- remove filter --'); + return this.getGroupBys(null, null, this.defaultRemoveFilterValue, false); } if (segment.type === 'value') { const filterKey = this.filterSegments[index - 2].value; + const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7); - if (this.metricLabels[filterKey]) { - return this.getValuesForFilterKey(this.metricLabels[filterKey]); + if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) { + return this.getValuesForFilterKey(this.metricLabels[shortKey]); } - if (this.resourceLabels[filterKey]) { - return this.getValuesForFilterKey(this.resourceLabels[filterKey]); + if (filterKey.startsWith('resource.label.') && this.resourceLabels.hasOwnProperty(shortKey)) { + return this.getValuesForFilterKey(this.resourceLabels[shortKey]); } } @@ -254,6 +271,42 @@ export class StackdriverQueryCtrl extends QueryCtrl { return filterValues; } + filterSegmentUpdated(segment, index) { + if (segment.type === 'plus-button') { + this.addNewFilterSegments(segment, index); + } else if (segment.type === 'key' && segment.value === this.defaultRemoveFilterValue) { + this.removeFilterSegment(index); + this.ensurePlusButton(this.filterSegments); + } else if (segment.type === 'value' && segment.value !== this.defaultFilterValue) { + this.ensurePlusButton(this.filterSegments); + } + + this.target.filters = this.filterSegments.filter(s => s.type !== 'plus-button').map(seg => seg.value); + this.refresh(); + } + + addNewFilterSegments(segment, index) { + if (index > 2) { + this.filterSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); + } + segment.type = 'key'; + this.filterSegments.push(this.uiSegmentSrv.newOperator('=')); + this.filterSegments.push(this.uiSegmentSrv.newFake(this.defaultFilterValue, 'value', 'query-segment-value')); + } + + removeFilterSegment(index) { + this.filterSegments.splice(index, 3); + // remove trailing condition + if (index > 2 && this.filterSegments[index - 1].type === 'condition') { + this.filterSegments.splice(index - 1, 1); + } + + // remove condition if it is first segment + if (index === 0 && this.filterSegments[0].type === 'condition') { + this.filterSegments.splice(0, 1); + } + } + ensurePlusButton(segments) { const count = segments.length; const lastSegment = segments[Math.max(count - 1, 0)]; diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts index 3afc640149a..5c52fcfa8a6 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts @@ -4,11 +4,30 @@ describe('StackdriverQueryCtrl', () => { let ctrl; let result; - beforeEach(() => { - ctrl = createCtrlWithFakes(); + describe('when initializing query editor', () => { + beforeEach(() => { + const existingFilters = ['key1', '=', 'val1', 'AND', 'key2', '=', 'val2']; + ctrl = createCtrlWithFakes(existingFilters); + }); + + it('should initialize filter segments using the target filter values', () => { + expect(ctrl.filterSegments.length).toBe(8); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + expect(ctrl.filterSegments[3].type).toBe('condition'); + expect(ctrl.filterSegments[4].type).toBe('key'); + expect(ctrl.filterSegments[5].type).toBe('operator'); + expect(ctrl.filterSegments[6].type).toBe('value'); + expect(ctrl.filterSegments[7].type).toBe('plus-button'); + }); }); describe('group bys', () => { + beforeEach(() => { + ctrl = createCtrlWithFakes(); + }); + describe('when labels are fetched', () => { beforeEach(async () => { ctrl.metricLabels = { 'metric-key-1': ['metric-value-1'] }; @@ -76,6 +95,10 @@ describe('StackdriverQueryCtrl', () => { }); describe('filters', () => { + beforeEach(() => { + ctrl = createCtrlWithFakes(); + }); + describe('when values for a condition filter part are fetched', () => { beforeEach(async () => { const segment = { type: 'condition' }; @@ -139,7 +162,7 @@ describe('StackdriverQueryCtrl', () => { 'resource-key-2': ['resource-value-2'], }; - ctrl.filterSegments = [{ type: 'key', value: 'metric-key-1' }, { type: 'operator', value: '=' }]; + ctrl.filterSegments = [{ type: 'key', value: 'metric.label.metric-key-1' }, { type: 'operator', value: '=' }]; const segment = { type: 'value' }; result = await ctrl.getFilters(segment, 2); @@ -150,16 +173,186 @@ describe('StackdriverQueryCtrl', () => { expect(result[0].value).toBe('metric-value-1'); }); }); + + describe('when a filter is created by clicking on plus button', () => { + describe('and there are no other filters', () => { + beforeEach(() => { + const segment = { value: 'filterkey1', type: 'plus-button' }; + ctrl.filterSegments = [segment]; + ctrl.filterSegmentUpdated(segment, 0); + }); + + it('should transform the plus button segment to a key segment', () => { + expect(ctrl.filterSegments[0].type).toBe('key'); + }); + + it('should add an operator, value segment and plus button segment', () => { + expect(ctrl.filterSegments.length).toBe(3); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + }); + }); + }); + describe('when has one existing filter', () => { + describe('and user clicks on key segment', () => { + beforeEach(() => { + const existingKeySegment = { value: 'filterkey1', type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + const plusSegment = { value: '', type: 'plus-button' }; + ctrl.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment, plusSegment]; + ctrl.filterSegmentUpdated(existingKeySegment, 0); + }); + + it('should not add any new segments', () => { + expect(ctrl.filterSegments.length).toBe(4); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + }); + }); + describe('and user clicks on value segment and value not equal to fake value', () => { + beforeEach(() => { + const existingKeySegment = { value: 'filterkey1', type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + ctrl.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment]; + ctrl.filterSegmentUpdated(existingValueSegment, 2); + }); + + it('should ensure that plus segment exists', () => { + expect(ctrl.filterSegments.length).toBe(4); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + expect(ctrl.filterSegments[3].type).toBe('plus-button'); + }); + }); + + describe('and user clicks on value segment and value is equal to fake value', () => { + beforeEach(() => { + const existingKeySegment = { value: 'filterkey1', type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: ctrl.defaultFilterValue, type: 'value' }; + ctrl.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment]; + ctrl.filterSegmentUpdated(existingValueSegment, 2); + }); + + it('should not add plus segment', () => { + expect(ctrl.filterSegments.length).toBe(3); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + }); + }); + describe('and user removes key segment', () => { + beforeEach(() => { + const existingKeySegment = { value: ctrl.defaultRemoveFilterValue, type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + const plusSegment = { value: '', type: 'plus-button' }; + ctrl.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment, plusSegment]; + ctrl.filterSegmentUpdated(existingKeySegment, 0); + }); + + it('should remove filter segments', () => { + expect(ctrl.filterSegments.length).toBe(1); + expect(ctrl.filterSegments[0].type).toBe('plus-button'); + }); + }); + + describe('and user removes key segment and there is a previous filter', () => { + beforeEach(() => { + const existingKeySegment1 = { value: ctrl.defaultRemoveFilterValue, type: 'key' }; + const existingKeySegment2 = { value: ctrl.defaultRemoveFilterValue, type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + const conditionSegment = { value: 'AND', type: 'condition' }; + const plusSegment = { value: '', type: 'plus-button' }; + ctrl.filterSegments = [ + existingKeySegment1, + existingOperatorSegment, + existingValueSegment, + conditionSegment, + existingKeySegment2, + Object.assign({}, existingOperatorSegment), + Object.assign({}, existingValueSegment), + plusSegment, + ]; + ctrl.filterSegmentUpdated(existingKeySegment2, 4); + }); + + it('should remove filter segments and the condition segment', () => { + expect(ctrl.filterSegments.length).toBe(4); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + expect(ctrl.filterSegments[3].type).toBe('plus-button'); + }); + }); + + describe('and user removes key segment and there is a filter after it', () => { + beforeEach(() => { + const existingKeySegment1 = { value: ctrl.defaultRemoveFilterValue, type: 'key' }; + const existingKeySegment2 = { value: ctrl.defaultRemoveFilterValue, type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + const conditionSegment = { value: 'AND', type: 'condition' }; + const plusSegment = { value: '', type: 'plus-button' }; + ctrl.filterSegments = [ + existingKeySegment1, + existingOperatorSegment, + existingValueSegment, + conditionSegment, + existingKeySegment2, + Object.assign({}, existingOperatorSegment), + Object.assign({}, existingValueSegment), + plusSegment, + ]; + ctrl.filterSegmentUpdated(existingKeySegment1, 0); + }); + + it('should remove filter segments and the condition segment', () => { + expect(ctrl.filterSegments.length).toBe(4); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + expect(ctrl.filterSegments[3].type).toBe('plus-button'); + }); + }); + + describe('and user clicks on plus button', () => { + beforeEach(() => { + const existingKeySegment = { value: 'filterkey1', type: 'key' }; + const existingOperatorSegment = { value: '=', type: 'operator' }; + const existingValueSegment = { value: 'filtervalue', type: 'value' }; + const plusSegment = { value: 'filterkey2', type: 'plus-button' }; + ctrl.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment, plusSegment]; + ctrl.filterSegmentUpdated(plusSegment, 3); + }); + + it('should condition segment and new filter segments', () => { + expect(ctrl.filterSegments.length).toBe(7); + expect(ctrl.filterSegments[0].type).toBe('key'); + expect(ctrl.filterSegments[1].type).toBe('operator'); + expect(ctrl.filterSegments[2].type).toBe('value'); + expect(ctrl.filterSegments[3].type).toBe('condition'); + expect(ctrl.filterSegments[4].type).toBe('key'); + expect(ctrl.filterSegments[5].type).toBe('operator'); + expect(ctrl.filterSegments[6].type).toBe('value'); + }); + }); + }); }); }); -function createCtrlWithFakes() { +function createCtrlWithFakes(existingFilters?: string[]) { StackdriverQueryCtrl.prototype.panelCtrl = { events: { on: () => {} }, panel: { scopedVars: [], targets: [] }, refresh: () => {}, }; - StackdriverQueryCtrl.prototype.target = createTarget(); + StackdriverQueryCtrl.prototype.target = createTarget(existingFilters); StackdriverQueryCtrl.prototype.getMetricTypes = () => { return Promise.resolve(); }; @@ -168,20 +361,37 @@ function createCtrlWithFakes() { }; const fakeSegmentServer = { + newKey: val => { + return { value: val, type: 'key' }; + }, + newKeyValue: val => { + return { value: val, type: 'value' }; + }, newSegment: obj => { return { value: obj.value ? obj.value : obj }; }, newOperators: ops => { return ops.map(o => { - return { type: 'operator', value: o, text: o }; + return { type: 'operator', value: o }; }); }, - newPlusButton: () => {}, + newFake: (value, type, cssClass) => { + return { value, type, cssClass }; + }, + newOperator: op => { + return { value: op, type: 'operator' }; + }, + newPlusButton: () => { + return { type: 'plus-button' }; + }, + newCondition: val => { + return { type: 'condition', value: val }; + }, }; return new StackdriverQueryCtrl(null, null, fakeSegmentServer, null); } -function createTarget() { +function createTarget(existingFilters?: string[]) { return { project: { id: '', @@ -195,6 +405,6 @@ function createTarget() { perSeriesAligner: '', groupBys: [], }, - filters: [], + filters: existingFilters || [], }; }