mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 16:43:07 +08:00
stackdriver: add filters to query editor
WIP -> Backend not implemented yet.
This commit is contained in:
@ -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({
|
||||
|
@ -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)];
|
||||
|
@ -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 || [],
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user