grafana/public/app/plugins/datasource/elasticsearch/metric_agg.ts
Chris Cowan 3af193358c
Elasticsearch: Support chained pipeline aggregations (#27945)
* Elasticsearch: Support multiple pipeline aggregations for a query

* Ensuring descendents are deleted with their parents.

* Update public/app/plugins/datasource/elasticsearch/metric_agg.ts

Co-authored-by: Giordano Ricci <grdnricci@gmail.com>

* Update public/app/plugins/datasource/elasticsearch/query_def.ts

Co-authored-by: Giordano Ricci <grdnricci@gmail.com>

* Update public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts

Co-authored-by: Giordano Ricci <grdnricci@gmail.com>

* Fixing typo

Co-authored-by: Giordano Ricci <grdnricci@gmail.com>
2020-10-08 11:24:54 +01:00

246 lines
7.9 KiB
TypeScript

import coreModule from 'app/core/core_module';
import _ from 'lodash';
import * as queryDef from './query_def';
import { ElasticsearchAggregation } from './types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { CoreEvents } from 'app/types';
function createDefaultMetric(id = 0): ElasticsearchAggregation {
return { type: 'count', field: 'select field', id: (id + 1).toString() };
}
export class ElasticMetricAggCtrl {
/** @ngInject */
constructor($scope: any, uiSegmentSrv: any, $rootScope: GrafanaRootScope) {
const metricAggs: ElasticsearchAggregation[] = $scope.target.metrics;
$scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion);
$scope.extendedStats = queryDef.extendedStats;
$scope.pipelineAggOptions = [];
$scope.modelSettingsValues = {};
$scope.init = () => {
$scope.agg = metricAggs[$scope.index];
$scope.validateModel();
$scope.updatePipelineAggOptions();
};
$scope.updatePipelineAggOptions = () => {
$scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target, $scope.agg);
};
$rootScope.onAppEvent(
CoreEvents.elasticQueryUpdated,
() => {
$scope.index = _.indexOf(metricAggs, $scope.agg);
$scope.updatePipelineAggOptions();
$scope.validateModel();
},
$scope
);
$scope.validateModel = () => {
$scope.isFirst = $scope.index === 0;
$scope.isSingle = metricAggs.length === 1;
$scope.settingsLinkText = '';
$scope.variablesLinkText = '';
$scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
if (queryDef.isPipelineAgg($scope.agg.type)) {
if (queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type)) {
$scope.variablesLinkText = 'Options';
if ($scope.agg.settings.script) {
$scope.variablesLinkText = 'Script: ' + $scope.agg.settings.script.replace(new RegExp('params.', 'g'), '');
}
} else {
$scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
$scope.agg.field = $scope.agg.pipelineAgg;
}
const pipelineOptions = queryDef.getPipelineOptions($scope.agg);
if (pipelineOptions.length > 0) {
_.each(pipelineOptions, opt => {
$scope.agg.settings[opt.text] = $scope.agg.settings[opt.text] || opt.default;
});
$scope.settingsLinkText = 'Options';
}
} else if (!$scope.agg.field) {
$scope.agg.field = 'select field';
}
switch ($scope.agg.type) {
case 'cardinality': {
const precisionThreshold = $scope.agg.settings.precision_threshold || '';
$scope.settingsLinkText = 'Precision threshold: ' + precisionThreshold;
break;
}
case 'percentiles': {
$scope.agg.settings.percents = $scope.agg.settings.percents || [25, 50, 75, 95, 99];
$scope.settingsLinkText = 'Values: ' + $scope.agg.settings.percents.join(',');
break;
}
case 'extended_stats': {
if (_.keys($scope.agg.meta).length === 0) {
$scope.agg.meta.std_deviation_bounds_lower = true;
$scope.agg.meta.std_deviation_bounds_upper = true;
}
const stats = _.reduce(
$scope.agg.meta,
(memo, val, key) => {
if (val) {
const def: any = _.find($scope.extendedStats, { value: key });
memo.push(def.text);
}
return memo;
},
[] as string[]
);
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
break;
}
case 'moving_avg': {
$scope.movingAvgModelTypes = queryDef.movingAvgModelOptions;
$scope.modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, true);
$scope.updateMovingAvgModelSettings();
break;
}
case 'raw_document':
case 'raw_data': {
$scope.agg.settings.size = $scope.agg.settings.size || 500;
$scope.settingsLinkText = 'Size: ' + $scope.agg.settings.size;
$scope.target.metrics.splice(0, $scope.target.metrics.length, $scope.agg);
$scope.target.bucketAggs = [];
break;
}
}
if ($scope.aggDef.supportsInlineScript) {
// I know this stores the inline script twice
// but having it like this simplifes the query_builder
const inlineScript = $scope.agg.inlineScript;
if (inlineScript) {
$scope.agg.settings.script = { inline: inlineScript };
} else {
delete $scope.agg.settings.script;
}
if ($scope.settingsLinkText === '') {
$scope.settingsLinkText = 'Options';
}
}
};
$scope.toggleOptions = () => {
$scope.showOptions = !$scope.showOptions;
$scope.updatePipelineAggOptions();
};
$scope.toggleVariables = () => {
$scope.showVariables = !$scope.showVariables;
};
$scope.onChangeInternal = () => {
$scope.onChange();
};
$scope.updateMovingAvgModelSettings = () => {
const modelSettingsKeys = [];
const modelSettings = queryDef.getMovingAvgSettings($scope.agg.settings.model, false);
for (let i = 0; i < modelSettings.length; i++) {
modelSettingsKeys.push(modelSettings[i].value);
}
for (const key in $scope.agg.settings.settings) {
if ($scope.agg.settings.settings[key] === null || modelSettingsKeys.indexOf(key) === -1) {
delete $scope.agg.settings.settings[key];
}
}
};
$scope.onChangeClearInternal = () => {
delete $scope.agg.settings.minimize;
$scope.onChange();
};
$scope.onTypeChange = () => {
$scope.agg.settings = {};
$scope.agg.meta = {};
$scope.showOptions = false;
// reset back to metric/group by query
if (
$scope.target.bucketAggs.length === 0 &&
($scope.agg.type !== 'raw_document' || $scope.agg.type !== 'raw_data')
) {
$scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
}
$scope.showVariables = queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type);
$scope.updatePipelineAggOptions();
$scope.onChange();
};
$scope.getFieldsInternal = () => {
if ($scope.agg.type === 'cardinality') {
return $scope.getFields();
}
return $scope.getFields({ $fieldType: 'number' });
};
$scope.addMetricAgg = () => {
const addIndex = metricAggs.length;
const id = _.reduce(
$scope.target.bucketAggs.concat($scope.target.metrics),
(max, val) => {
return parseInt(val.id, 10) > max ? parseInt(val.id, 10) : max;
},
0
);
metricAggs.splice(addIndex, 0, createDefaultMetric(id));
$scope.onChange();
};
$scope.removeMetricAgg = () => {
const metricBeingRemoved = metricAggs[$scope.index];
const metricsToRemove = queryDef.getAncestors($scope.target, metricBeingRemoved);
const newMetricAggs = metricAggs.filter(m => !metricsToRemove.includes(m.id));
if (newMetricAggs.length > 0) {
metricAggs.splice(0, metricAggs.length, ...newMetricAggs);
} else {
metricAggs.splice(0, metricAggs.length, createDefaultMetric());
}
$scope.onChange();
};
$scope.toggleShowMetric = () => {
$scope.agg.hide = !$scope.agg.hide;
if (!$scope.agg.hide) {
delete $scope.agg.hide;
}
$scope.onChange();
};
$scope.init();
}
}
export function elasticMetricAgg() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: ElasticMetricAggCtrl,
restrict: 'E',
scope: {
target: '=',
index: '=',
onChange: '&',
getFields: '&',
esVersion: '=',
},
};
}
coreModule.directive('elasticMetricAgg', elasticMetricAgg);