Browse Source

stackdriver: add filters to query editor

WIP -> Backend not implemented yet.
Daniel Lee 7 years ago
parent
commit
dc6d025d9a

+ 1 - 0
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({

+ 72 - 19
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)];

+ 219 - 9
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 || [],
   };
 }