diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index f3d0b81998f..6b02e906583 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -3,9 +3,11 @@ export default class TableModel { columns: any[]; rows: any[]; type: string; + columnMap: any; constructor() { this.columns = []; + this.columnMap = {}; this.rows = []; this.type = 'table'; } @@ -36,4 +38,11 @@ export default class TableModel { this.columns[options.col].desc = false; } } + + addColumn(col) { + if (!this.columnMap[col.text]) { + this.columns.push(col); + this.columnMap[col.text] = col; + } + } } diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index f962f8ca2f4..47babfdd5dd 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -10,9 +10,10 @@ export class AdHocFiltersCtrl { removeTagFilterSegment: any; /** @ngInject */ - constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) { + constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, private $scope, private $rootScope) { this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'}); this.buildSegmentModel(); + this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); } buildSegmentModel() { @@ -141,8 +142,7 @@ export class AdHocFiltersCtrl { } this.variable.setFilters(filters); - this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh'); + this.variableSrv.variableUpdated(this.variable, true); } } diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index a925ba97f0d..0082f120b5f 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -22,10 +22,7 @@ export class SubmenuCtrl { } variableUpdated(variable) { - this.variableSrv.variableUpdated(variable).then(() => { - this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh'); - }); + this.variableSrv.variableUpdated(variable, true); } openEditView(editview) { diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 97274cb30d0..99c235ef24c 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -55,9 +55,8 @@ export class VariableEditorCtrl { $scope.add = function() { if ($scope.isValid()) { - $scope.variables.push($scope.current); + variableSrv.addVariable($scope.current); $scope.update(); - $scope.dashboard.updateSubmenuVisibility(); } }; @@ -114,9 +113,8 @@ export class VariableEditorCtrl { $scope.duplicate = function(variable) { var clone = _.cloneDeep(variable.getSaveModel()); $scope.current = variableSrv.createVariableFromModel(clone); - $scope.variables.push($scope.current); $scope.current.name = 'copy_of_'+variable.name; - $scope.dashboard.updateSubmenuVisibility(); + $scope.variableSrv.addVariable($scope.current); }; $scope.update = function() { @@ -150,9 +148,7 @@ export class VariableEditorCtrl { }; $scope.removeVariable = function(variable) { - var index = _.indexOf($scope.variables, variable); - $scope.variables.splice(index, 1); - $scope.dashboard.updateSubmenuVisibility(); + variableSrv.removeVariable(variable); }; } } diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts index 85bca8d6068..9afb38056bd 100644 --- a/public/app/features/templating/specs/variable_srv_specs.ts +++ b/public/app/features/templating/specs/variable_srv_specs.ts @@ -22,6 +22,7 @@ describe('VariableSrv', function() { ctx.variableSrv.init({ templating: {list: []}, events: new Emitter(), + updateSubmenuVisibility: sinon.stub(), }); ctx.$rootScope.$digest(); })); @@ -41,7 +42,9 @@ describe('VariableSrv', function() { ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources); - scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel); + scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); + ctx.variableSrv.addVariable(scenario.variable); + ctx.variableSrv.updateOptions(scenario.variable); ctx.$rootScope.$digest(); }); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 022203b9eb9..c147c5b6685 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -90,17 +90,24 @@ export class VariableSrv { return variable; } - addVariable(model) { - var variable = this.createVariableFromModel(model); + addVariable(variable) { this.variables.push(variable); - return variable; + this.templateSrv.updateTemplateData(); + this.dashboard.updateSubmenuVisibility(); + } + + removeVariable(variable) { + var index = _.indexOf(this.variables, variable); + this.variables.splice(index, 1); + this.templateSrv.updateTemplateData(); + this.dashboard.updateSubmenuVisibility(); } updateOptions(variable) { return variable.updateOptions(); } - variableUpdated(variable) { + variableUpdated(variable, emitChangeEvents?) { // if there is a variable lock ignore cascading update because we are in a boot up scenario if (variable.initLock) { return this.$q.when(); @@ -117,7 +124,12 @@ export class VariableSrv { } }); - return this.$q.all(promises); + return this.$q.all(promises).then(() => { + if (emitChangeEvents) { + this.$rootScope.$emit('template-variable-value-updated'); + this.$rootScope.$broadcast('refresh'); + } + }); } selectOptionsForCurrentValue(variable) { @@ -218,6 +230,28 @@ export class VariableSrv { // update url this.$location.search(params); } + + setAdhocFilter(options) { + var variable = _.find(this.variables, {type: 'adhoc', datasource: options.datasource}); + if (!variable) { + variable = this.createVariableFromModel({name: 'Filters', type: 'adhoc', datasource: options.datasource}); + this.addVariable(variable); + } + + let filters = variable.filters; + let filter = _.find(filters, {key: options.key, value: options.value}); + + if (!filter) { + filter = {key: options.key, value: options.value}; + filters.push(filter); + } + + filter.operator = options.operator; + + variable.setFilters(filters); + this.variableUpdated(variable, true); + } + } coreModule.service('variableSrv', VariableSrv); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 49398a894e7..9ea679d5cc5 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -11,6 +11,8 @@ define([ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) { 'use strict'; + ElasticResponse = ElasticResponse.ElasticResponse; + /** @ngInject */ function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) { this.basicAuth = instanceSettings.basicAuth; @@ -270,10 +272,17 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var subObj = obj[key]; // Check mapping field for nested fields - if (subObj.hasOwnProperty('properties')) { + if (_.isObject(subObj.properties)) { fieldNameParts.push(key); getFieldsRecursively(subObj.properties); - } else { + } + + if (_.isObject(subObj.fields)) { + fieldNameParts.push(key); + getFieldsRecursively(subObj.fields); + } + + if (_.isString(subObj.type)) { var fieldName = fieldNameParts.concat(key).join('.'); // Hide meta-fields and check field type diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts deleted file mode 100644 index c3318b8e133..00000000000 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare var test: any; -export default test; diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.js b/public/app/plugins/datasource/elasticsearch/elastic_response.js deleted file mode 100644 index 9e944774dc9..00000000000 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.js +++ /dev/null @@ -1,350 +0,0 @@ -define([ - "lodash", - "./query_def" -], -function (_, queryDef) { - 'use strict'; - - function ElasticResponse(targets, response) { - this.targets = targets; - this.response = response; - } - - ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) { - var metric, y, i, newSeries, bucket, value; - - for (y = 0; y < target.metrics.length; y++) { - metric = target.metrics[y]; - if (metric.hide) { - continue; - } - - switch(metric.type) { - case 'count': { - newSeries = { datapoints: [], metric: 'count', props: props}; - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - value = bucket.doc_count; - newSeries.datapoints.push([value, bucket.key]); - } - seriesList.push(newSeries); - break; - } - case 'percentiles': { - if (esAgg.buckets.length === 0) { - break; - } - - var firstBucket = esAgg.buckets[0]; - var percentiles = firstBucket[metric.id].values; - - for (var percentileName in percentiles) { - newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field}; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - var values = bucket[metric.id].values; - newSeries.datapoints.push([values[percentileName], bucket.key]); - } - seriesList.push(newSeries); - } - - break; - } - case 'extended_stats': { - for (var statName in metric.meta) { - if (!metric.meta[statName]) { - continue; - } - - newSeries = {datapoints: [], metric: statName, props: props, field: metric.field}; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - var stats = bucket[metric.id]; - - // add stats that are in nested obj to top level obj - stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; - stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; - - newSeries.datapoints.push([stats[statName], bucket.key]); - } - - seriesList.push(newSeries); - } - - break; - } - default: { - newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - - value = bucket[metric.id]; - if (value !== undefined) { - if (value.normalized_value) { - newSeries.datapoints.push([value.normalized_value, bucket.key]); - } else { - newSeries.datapoints.push([value.value, bucket.key]); - } - } - - } - seriesList.push(newSeries); - break; - } - } - } - }; - - ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, docs, props) { - var metric, y, i, bucket, metricName, doc; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - doc = _.defaults({}, props); - doc[aggDef.field] = bucket.key; - - for (y = 0; y < target.metrics.length; y++) { - metric = target.metrics[y]; - - switch(metric.type) { - case "count": { - metricName = this._getMetricName(metric.type); - doc[metricName] = bucket.doc_count; - break; - } - case 'extended_stats': { - for (var statName in metric.meta) { - if (!metric.meta[statName]) { - continue; - } - - var stats = bucket[metric.id]; - // add stats that are in nested obj to top level obj - stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; - stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; - - metricName = this._getMetricName(statName); - doc[metricName] = stats[statName]; - } - break; - } - default: { - metricName = this._getMetricName(metric.type); - var otherMetrics = _.filter(target.metrics, {type: metric.type}); - - // if more of the same metric type include field field name in property - if (otherMetrics.length > 1) { - metricName += ' ' + metric.field; - } - - doc[metricName] = bucket[metric.id].value; - break; - } - } - } - - docs.push(doc); - } - }; - - // This is quite complex - // neeed to recurise down the nested buckets to build series - ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, docs, props, depth) { - var bucket, aggDef, esAgg, aggId; - var maxDepth = target.bucketAggs.length-1; - - for (aggId in aggs) { - aggDef = _.find(target.bucketAggs, {id: aggId}); - esAgg = aggs[aggId]; - - if (!aggDef) { - continue; - } - - if (depth === maxDepth) { - if (aggDef.type === 'date_histogram') { - this.processMetrics(esAgg, target, seriesList, props); - } else { - this.processAggregationDocs(esAgg, aggDef, target, docs, props); - } - } else { - for (var nameIndex in esAgg.buckets) { - bucket = esAgg.buckets[nameIndex]; - props = _.clone(props); - if (bucket.key !== void 0) { - props[aggDef.field] = bucket.key; - } else { - props["filter"] = nameIndex; - } - if (bucket.key_as_string) { - props[aggDef.field] = bucket.key_as_string; - } - this.processBuckets(bucket, target, seriesList, docs, props, depth+1); - } - } - } - }; - - ElasticResponse.prototype._getMetricName = function(metric) { - var metricDef = _.find(queryDef.metricAggTypes, {value: metric}); - if (!metricDef) { - metricDef = _.find(queryDef.extendedStats, {value: metric}); - } - - return metricDef ? metricDef.text : metric; - }; - - ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) { - var metricName = this._getMetricName(series.metric); - - if (target.alias) { - var regex = /\{\{([\s\S]+?)\}\}/g; - - return target.alias.replace(regex, function(match, g1, g2) { - var group = g1 || g2; - - if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; } - if (series.props[group] !== void 0) { return series.props[group]; } - if (group === 'metric') { return metricName; } - if (group === 'field') { return series.field; } - - return match; - }); - } - - if (series.field && queryDef.isPipelineAgg(series.metric)) { - var appliedAgg = _.find(target.metrics, { id: series.field }); - if (appliedAgg) { - metricName += ' ' + queryDef.describeMetric(appliedAgg); - } else { - metricName = 'Unset'; - } - } else if (series.field) { - metricName += ' ' + series.field; - } - - var propKeys = _.keys(series.props); - if (propKeys.length === 0) { - return metricName; - } - - var name = ''; - for (var propName in series.props) { - name += series.props[propName] + ' '; - } - - if (metricTypeCount === 1) { - return name.trim(); - } - - return name.trim() + ' ' + metricName; - }; - - ElasticResponse.prototype.nameSeries = function(seriesList, target) { - var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length; - var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length; - - for (var i = 0; i < seriesList.length; i++) { - var series = seriesList[i]; - series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount); - } - }; - - ElasticResponse.prototype.processHits = function(hits, seriesList) { - var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total}; - var propName, hit, doc, i; - - for (i = 0; i < hits.hits.length; i++) { - hit = hits.hits[i]; - doc = { - _id: hit._id, - _type: hit._type, - _index: hit._index - }; - - if (hit._source) { - for (propName in hit._source) { - doc[propName] = hit._source[propName]; - } - } - - for (propName in hit.fields) { - doc[propName] = hit.fields[propName]; - } - series.datapoints.push(doc); - } - - seriesList.push(series); - }; - - ElasticResponse.prototype.trimDatapoints = function(aggregations, target) { - var histogram = _.find(target.bucketAggs, { type: 'date_histogram'}); - - var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges; - if (shouldDropFirstAndLast) { - var trim = histogram.settings.trimEdges; - for(var prop in aggregations) { - var points = aggregations[prop]; - if (points.datapoints.length > trim * 2) { - points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim); - } - } - } - }; - - ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) { - var result = {}; - result.data = JSON.stringify(err, null, 4); - if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) { - result.message = err.root_cause[0].reason; - } else { - result.message = err.reason || 'Unkown elatic error response'; - } - - if (response.$$config) { - result.config = response.$$config; - } - - return result; - }; - - ElasticResponse.prototype.getTimeSeries = function() { - var seriesList = []; - - for (var i = 0; i < this.response.responses.length; i++) { - var response = this.response.responses[i]; - if (response.error) { - throw this.getErrorFromElasticResponse(this.response, response.error); - } - - if (response.hits && response.hits.hits.length > 0) { - this.processHits(response.hits, seriesList); - } - - if (response.aggregations) { - var aggregations = response.aggregations; - var target = this.targets[i]; - var tmpSeriesList = []; - var docs = []; - - this.processBuckets(aggregations, target, tmpSeriesList, docs, {}, 0); - this.trimDatapoints(tmpSeriesList, target); - this.nameSeries(tmpSeriesList, target); - - for (var y = 0; y < tmpSeriesList.length; y++) { - seriesList.push(tmpSeriesList[y]); - } - - if (seriesList.length === 0 && docs.length > 0) { - seriesList.push({target: 'docs', type: 'docs', datapoints: docs}); - } - } - } - - return { data: seriesList }; - }; - - return ElasticResponse; -}); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts new file mode 100644 index 00000000000..47fa14a99a6 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -0,0 +1,360 @@ +/// + +import _ from 'lodash'; +import queryDef from "./query_def"; +import TableModel from 'app/core/table_model'; + +export function ElasticResponse(targets, response) { + this.targets = targets; + this.response = response; +} + +ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) { + var metric, y, i, newSeries, bucket, value; + + for (y = 0; y < target.metrics.length; y++) { + metric = target.metrics[y]; + if (metric.hide) { + continue; + } + + switch (metric.type) { + case 'count': { + newSeries = { datapoints: [], metric: 'count', props: props}; + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + value = bucket.doc_count; + newSeries.datapoints.push([value, bucket.key]); + } + seriesList.push(newSeries); + break; + } + case 'percentiles': { + if (esAgg.buckets.length === 0) { + break; + } + + var firstBucket = esAgg.buckets[0]; + var percentiles = firstBucket[metric.id].values; + + for (var percentileName in percentiles) { + newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field}; + + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + var values = bucket[metric.id].values; + newSeries.datapoints.push([values[percentileName], bucket.key]); + } + seriesList.push(newSeries); + } + + break; + } + case 'extended_stats': { + for (var statName in metric.meta) { + if (!metric.meta[statName]) { + continue; + } + + newSeries = {datapoints: [], metric: statName, props: props, field: metric.field}; + + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + var stats = bucket[metric.id]; + + // add stats that are in nested obj to top level obj + stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; + stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; + + newSeries.datapoints.push([stats[statName], bucket.key]); + } + + seriesList.push(newSeries); + } + + break; + } + default: { + newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + + value = bucket[metric.id]; + if (value !== undefined) { + if (value.normalized_value) { + newSeries.datapoints.push([value.normalized_value, bucket.key]); + } else { + newSeries.datapoints.push([value.value, bucket.key]); + } + } + + } + seriesList.push(newSeries); + break; + } + } + } +}; + +ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, table, props) { + // add columns + if (table.columns.length === 0) { + for (let propKey of _.keys(props)) { + table.addColumn({text: propKey, filterable: true}); + } + table.addColumn({text: aggDef.field, filterable: true}); + } + + // helper func to add values to value array + let addMetricValue = (values, metricName, value) => { + table.addColumn({text: metricName}); + values.push(value); + }; + + for (let bucket of esAgg.buckets) { + let values = []; + + for (let propValues of _.values(props)) { + values.push(propValues); + } + + // add bucket key (value) + values.push(bucket.key); + + for (let metric of target.metrics) { + switch (metric.type) { + case "count": { + addMetricValue(values, this._getMetricName(metric.type), bucket.doc_count); + break; + } + case 'extended_stats': { + for (var statName in metric.meta) { + if (!metric.meta[statName]) { + continue; + } + + var stats = bucket[metric.id]; + // add stats that are in nested obj to top level obj + stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; + stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; + + addMetricValue(values, this._getMetricName(statName), stats[statName]); + } + break; + } + default: { + let metricName = this._getMetricName(metric.type); + let otherMetrics = _.filter(target.metrics, {type: metric.type}); + + // if more of the same metric type include field field name in property + if (otherMetrics.length > 1) { + metricName += ' ' + metric.field; + } + + addMetricValue(values, metricName, bucket[metric.id].value); + break; + } + } + } + + table.rows.push(values); + } +}; + +// This is quite complex +// neeed to recurise down the nested buckets to build series +ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, table, props, depth) { + var bucket, aggDef, esAgg, aggId; + var maxDepth = target.bucketAggs.length-1; + + for (aggId in aggs) { + aggDef = _.find(target.bucketAggs, {id: aggId}); + esAgg = aggs[aggId]; + + if (!aggDef) { + continue; + } + + if (depth === maxDepth) { + if (aggDef.type === 'date_histogram') { + this.processMetrics(esAgg, target, seriesList, props); + } else { + this.processAggregationDocs(esAgg, aggDef, target, table, props); + } + } else { + for (var nameIndex in esAgg.buckets) { + bucket = esAgg.buckets[nameIndex]; + props = _.clone(props); + if (bucket.key !== void 0) { + props[aggDef.field] = bucket.key; + } else { + props["filter"] = nameIndex; + } + if (bucket.key_as_string) { + props[aggDef.field] = bucket.key_as_string; + } + this.processBuckets(bucket, target, seriesList, table, props, depth+1); + } + } + } +}; + +ElasticResponse.prototype._getMetricName = function(metric) { + var metricDef = _.find(queryDef.metricAggTypes, {value: metric}); + if (!metricDef) { + metricDef = _.find(queryDef.extendedStats, {value: metric}); + } + + return metricDef ? metricDef.text : metric; +}; + +ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) { + var metricName = this._getMetricName(series.metric); + + if (target.alias) { + var regex = /\{\{([\s\S]+?)\}\}/g; + + return target.alias.replace(regex, function(match, g1, g2) { + var group = g1 || g2; + + if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; } + if (series.props[group] !== void 0) { return series.props[group]; } + if (group === 'metric') { return metricName; } + if (group === 'field') { return series.field; } + + return match; + }); + } + + if (series.field && queryDef.isPipelineAgg(series.metric)) { + var appliedAgg = _.find(target.metrics, { id: series.field }); + if (appliedAgg) { + metricName += ' ' + queryDef.describeMetric(appliedAgg); + } else { + metricName = 'Unset'; + } + } else if (series.field) { + metricName += ' ' + series.field; + } + + var propKeys = _.keys(series.props); + if (propKeys.length === 0) { + return metricName; + } + + var name = ''; + for (var propName in series.props) { + name += series.props[propName] + ' '; + } + + if (metricTypeCount === 1) { + return name.trim(); + } + + return name.trim() + ' ' + metricName; +}; + +ElasticResponse.prototype.nameSeries = function(seriesList, target) { + var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length; + var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length; + + for (var i = 0; i < seriesList.length; i++) { + var series = seriesList[i]; + series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount); + } +}; + +ElasticResponse.prototype.processHits = function(hits, seriesList) { + var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total, filterable: true}; + var propName, hit, doc, i; + + for (i = 0; i < hits.hits.length; i++) { + hit = hits.hits[i]; + doc = { + _id: hit._id, + _type: hit._type, + _index: hit._index + }; + + if (hit._source) { + for (propName in hit._source) { + doc[propName] = hit._source[propName]; + } + } + + for (propName in hit.fields) { + doc[propName] = hit.fields[propName]; + } + series.datapoints.push(doc); + } + + seriesList.push(series); +}; + +ElasticResponse.prototype.trimDatapoints = function(aggregations, target) { + var histogram = _.find(target.bucketAggs, { type: 'date_histogram'}); + + var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges; + if (shouldDropFirstAndLast) { + var trim = histogram.settings.trimEdges; + for (var prop in aggregations) { + var points = aggregations[prop]; + if (points.datapoints.length > trim * 2) { + points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim); + } + } + } +}; + +ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) { + var result: any = {}; + result.data = JSON.stringify(err, null, 4); + if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) { + result.message = err.root_cause[0].reason; + } else { + result.message = err.reason || 'Unkown elatic error response'; + } + + if (response.$$config) { + result.config = response.$$config; + } + + return result; +}; + +ElasticResponse.prototype.getTimeSeries = function() { + var seriesList = []; + + for (var i = 0; i < this.response.responses.length; i++) { + var response = this.response.responses[i]; + if (response.error) { + throw this.getErrorFromElasticResponse(this.response, response.error); + } + + if (response.hits && response.hits.hits.length > 0) { + this.processHits(response.hits, seriesList); + } + + if (response.aggregations) { + var aggregations = response.aggregations; + var target = this.targets[i]; + var tmpSeriesList = []; + var table = new TableModel(); + + this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0); + this.trimDatapoints(tmpSeriesList, target); + this.nameSeries(tmpSeriesList, target); + + for (var y = 0; y < tmpSeriesList.length; y++) { + seriesList.push(tmpSeriesList[y]); + } + + if (table.rows.length > 0) { + seriesList.push(table); + } + } + } + + return { data: seriesList }; +}; + diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index d0133256982..0838dc33654 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -129,7 +129,10 @@ describe('ElasticDatasource', function() { '@timestamp': {type: 'date'}, beat: { properties: { - name: {type: 'string'}, + name: { + fields: {raw: {type: 'keyword'}}, + type: 'string' + }, hostname: {type: 'string'}, } }, @@ -169,6 +172,7 @@ describe('ElasticDatasource', function() { var fields = _.map(fieldObjects, 'text'); expect(fields).to.eql([ '@timestamp', + 'beat.name.raw', 'beat.name', 'beat.hostname', 'system.cpu.system', diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts index bd89055c3b4..fde876af4f7 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts @@ -1,6 +1,6 @@ import {describe, beforeEach, it, expect} from 'test/lib/common'; -import ElasticResponse from '../elastic_response'; +import {ElasticResponse} from '../elastic_response'; describe('ElasticResponse', function() { var targets; @@ -387,10 +387,9 @@ describe('ElasticResponse', function() { result = new ElasticResponse(targets, response).getTimeSeries(); }); - it('should return docs with byte and count', function() { - expect(result.data[0].datapoints.length).to.be(3); - expect(result.data[0].datapoints[0].Count).to.be(1); - expect(result.data[0].datapoints[0].bytes).to.be(1000); + it('should return table with byte and count', function() { + expect(result.data[0].rows.length).to.be(3); + expect(result.data[0].columns).to.eql([{text: 'bytes', filterable: true}, {text: 'Count'}]); }); }); @@ -530,14 +529,14 @@ describe('ElasticResponse', function() { it('should return table', function() { expect(result.data.length).to.be(1); - expect(result.data[0].type).to.be('docs'); - expect(result.data[0].datapoints.length).to.be(2); - expect(result.data[0].datapoints[0].host).to.be("server-1"); - expect(result.data[0].datapoints[0].Average).to.be(1000); - expect(result.data[0].datapoints[0].Count).to.be(369); + expect(result.data[0].type).to.be('table'); + expect(result.data[0].rows.length).to.be(2); + expect(result.data[0].rows[0][0]).to.be("server-1"); + expect(result.data[0].rows[0][1]).to.be(1000); + expect(result.data[0].rows[0][2]).to.be(369); - expect(result.data[0].datapoints[1].host).to.be("server-2"); - expect(result.data[0].datapoints[1].Average).to.be(2000); + expect(result.data[0].rows[1][0]).to.be("server-2"); + expect(result.data[0].rows[1][1]).to.be(2000); }); }); @@ -573,10 +572,9 @@ describe('ElasticResponse', function() { }); it('should include field in metric name', function() { - expect(result.data[0].type).to.be('docs'); - expect(result.data[0].datapoints[0].Average).to.be(undefined); - expect(result.data[0].datapoints[0]['Average test']).to.be(1000); - expect(result.data[0].datapoints[0]['Average test2']).to.be(3000); + expect(result.data[0].type).to.be('table'); + expect(result.data[0].rows[0][1]).to.be(1000); + expect(result.data[0].rows[0][2]).to.be(3000); }); }); diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html index 9854ac26dc3..36d78b09dfa 100644 --- a/public/app/plugins/panel/table/editor.html +++ b/public/app/plugins/panel/table/editor.html @@ -17,9 +17,17 @@ {{column.text}} -
+
+
+ +
diff --git a/public/app/plugins/panel/table/editor.ts b/public/app/plugins/panel/table/editor.ts index 9850d60c9ad..a4fd72ed6f2 100644 --- a/public/app/plugins/panel/table/editor.ts +++ b/public/app/plugins/panel/table/editor.ts @@ -16,6 +16,8 @@ export class TablePanelEditorCtrl { fontSizes: any; addColumnSegment: any; getColumnNames: any; + canSetColumns: boolean; + columnsHelpMessage: string; /** @ngInject */ constructor($scope, private $q, private uiSegmentSrv) { @@ -24,8 +26,27 @@ export class TablePanelEditorCtrl { this.panel = this.panelCtrl.panel; this.transformers = transformers; this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%']; - this.addColumnSegment = uiSegmentSrv.newPlusButton(); + this.updateTransformHints(); + } + + updateTransformHints() { + this.canSetColumns = false; + this.columnsHelpMessage = ''; + + switch (this.panel.transform) { + case "timeseries_aggregations": { + this.canSetColumns = true; + break; + } + case "json": { + this.canSetColumns = true; + break; + } + case "table": { + this.columnsHelpMessage = "Columns and their order are determined by the data query"; + } + } } getColumnOptions() { @@ -57,6 +78,7 @@ export class TablePanelEditorCtrl { this.panel.columns.push({text: 'Avg', value: 'avg'}); } + this.updateTransformHints(); this.render(); } diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 375d3a0a0ae..fdc36804684 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -50,8 +50,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize) { + constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize, private variableSrv) { super($scope, $injector); + this.pageIndex = 0; if (this.panel.styles === void 0) { @@ -223,10 +224,24 @@ class TablePanelCtrl extends MetricsPanelCtrl { selector: '[data-link-tooltip]' }); + function addFilterClicked(e) { + let filterData = $(e.currentTarget).data(); + var options = { + datasource: panel.datasource, + key: data.columns[filterData.column].text, + value: data.rows[filterData.row][filterData.column], + operator: filterData.operator, + }; + + ctrl.variableSrv.setAdhocFilter(options); + } + elem.on('click', '.table-panel-page-link', switchPage); + elem.on('click', '.table-panel-filter-link', addFilterClicked); var unbindDestroy = scope.$on('$destroy', function() { elem.off('click', '.table-panel-page-link'); + elem.off('click', '.table-panel-filter-link'); unbindDestroy(); }); diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index ef53b1d489b..02f0ceca72c 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -140,9 +140,12 @@ export class TableRenderer { renderCell(columnIndex, rowIndex, value, addWidthHack = false) { value = this.formatColumnValue(columnIndex, value); + + var column = this.table.columns[columnIndex]; var style = ''; var cellClasses = []; var cellClass = ''; + if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; this.colorState.cell = null; @@ -161,26 +164,25 @@ export class TableRenderer { if (value === undefined) { style = ' style="display:none;"'; - this.table.columns[columnIndex].hidden = true; + column.hidden = true; } else { - this.table.columns[columnIndex].hidden = false; + column.hidden = false; } - var columnStyle = this.table.columns[columnIndex].style; - if (columnStyle && columnStyle.preserveFormat) { + if (column.style && column.style.preserveFormat) { cellClasses.push("table-panel-cell-pre"); } - var columnHtml = value + widthHack; + var columnHtml = widthHack + value; - if (columnStyle && columnStyle.link) { + if (column.style && column.style.link) { // Render cell as link var scopedVars = this.renderRowVariables(rowIndex); scopedVars['__cell'] = { value: value }; - var cellLink = this.templateSrv.replace(columnStyle.linkUrl, scopedVars); - var cellLinkTooltip = this.templateSrv.replace(columnStyle.linkTooltip, scopedVars); - var cellTarget = columnStyle.linkTargetBlank ? '_blank' : ''; + var cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars); + var cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars); + var cellTarget = column.style.linkTargetBlank ? '_blank' : ''; cellClasses.push("table-panel-cell-link"); columnHtml = ` @@ -190,6 +192,19 @@ export class TableRenderer { `; } + if (column.filterable) { + cellClasses.push("table-panel-cell-filterable"); + columnHtml += ` + + + + + + `; + } + if (cellClasses.length) { cellClass = ' class="' + cellClasses.join(' ') + '"'; } diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 07d183eafa8..cb6d30e627d 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -185,8 +185,16 @@ transformers['json'] = { }, transform: function(data, panel, model) { var i, y, z; - for (i = 0; i < panel.columns.length; i++) { - model.columns.push({text: panel.columns[i].text}); + + for (let column of panel.columns) { + var tableCol: any = {text: column.text}; + + // if filterable data then set columns to filterable + if (data.length > 0 && data[0].filterable) { + tableCol.filterable = true; + } + + model.columns.push(tableCol); } if (model.columns.length === 0) { diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 073d8be4abf..cf81dca4465 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -91,9 +91,23 @@ &.cell-highlighted:hover { background-color: $tight-form-func-bg; } + + &:hover { + .table-panel-filter-link { + visibility: visible; + } + } } } +.table-panel-filter-link { + visibility: hidden; + color: $text-color-weak; + float: right; + display: block; + padding: 0 5px; +} + .table-panel-header-bg { background: $grafanaListAccent; border-top: 2px solid $body-bg;