Merge branch 'elastic_ds'

Conflicts:
	public/app/plugins/datasource/influxdb/queryCtrl.js
This commit is contained in:
Torkel Ödegaard
2015-09-08 09:17:34 +02:00
130 changed files with 27521 additions and 361 deletions

View File

@@ -45,6 +45,7 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
'aws-sdk': '../vendor/aws-sdk/dist/aws-sdk.min',
},
shim: {

View File

@@ -20,8 +20,8 @@ function (angular, app, _, $) {
return {
scope: {
segment: "=",
getAltSegments: "&",
onValueChanged: "&"
getOptions: "&",
onChange: "&",
},
link: function($scope, elem) {
@@ -47,13 +47,14 @@ function (angular, app, _, $) {
segment.fake = false;
segment.expandable = selected.expandable;
}
else {
else if (segment.custom !== 'false') {
segment.value = value;
segment.html = $sce.trustAsHtml(value);
segment.expandable = true;
segment.fake = false;
}
$scope.onValueChanged();
$scope.onChange();
});
};
@@ -76,13 +77,15 @@ function (angular, app, _, $) {
if (options) { return options; }
$scope.$apply(function() {
$scope.getAltSegments().then(function(altSegments) {
$scope.getOptions().then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) { return alt.value; });
// add custom values
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
options.unshift(segment.value);
if (segment.custom !== 'false') {
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
options.unshift(segment.value);
}
}
callback(options);
@@ -92,7 +95,6 @@ function (angular, app, _, $) {
$scope.updater = function(value) {
if (value === segment.value) {
console.log('cancel blur');
clearTimeout(cancelBlur);
$input.focus();
return value;
@@ -153,4 +155,63 @@ function (angular, app, _, $) {
}
};
});
angular
.module('grafana.directives')
.directive('metricSegmentModel', function(uiSegmentSrv, $q) {
return {
template: '<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
restrict: 'E',
scope: {
property: "=",
options: "=",
getOptions: "&",
onChange: "&",
},
link: {
pre: function postLink($scope, elem, attrs) {
$scope.valueToSegment = function(value) {
var option = _.findWhere($scope.options, {value: value});
var segment = {
cssClass: attrs.cssClass,
custom: attrs.custom,
value: option ? option.text : value,
};
return uiSegmentSrv.newSegment(segment);
};
$scope.getOptionsInternal = function() {
if ($scope.options) {
var optionSegments = _.map($scope.options, function(option) {
return uiSegmentSrv.newSegment({value: option.text});
});
return $q.when(optionSegments);
} else {
return $scope.getOptions();
}
};
$scope.onSegmentChange = function() {
if ($scope.options) {
var option = _.findWhere($scope.options, {text: $scope.segment.value});
if (option && option.value !== $scope.property) {
$scope.property = option.value;
}
} else {
$scope.property = $scope.segment.value;
}
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(function() {
$scope.onChange();
});
};
$scope.segment = $scope.valueToSegment($scope.property);
}
}
};
});
});

View File

@@ -13,6 +13,7 @@ function (angular, kbn) {
link: function(scope, elem, attrs) {
var _t = '<i class="grafana-tip fa fa-'+(attrs.icon||'question-circle')+'" bs-tooltip="\''+
kbn.addslashes(elem.text())+'\'"></i>';
_t = _t.replace(/{/g, '\\{').replace(/}/g, '\\}');
elem.replaceWith($compile(angular.element(_t))(scope));
}
};
@@ -62,15 +63,16 @@ function (angular, kbn) {
restrict: 'E',
link: function(scope, elem, attrs) {
var text = $interpolate(attrs.text)(scope);
var model = $interpolate(attrs.model)(scope);
var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
var label = '<label for="' + scope.$id + attrs.model + '" class="checkbox-label">' +
var label = '<label for="' + scope.$id + model + '" class="checkbox-label">' +
text + tip + '</label>';
var template = '<input class="cr1" id="' + scope.$id + attrs.model + '" type="checkbox" ' +
' ng-model="' + attrs.model + '"' + ngchange +
' ng-checked="' + attrs.model + '"></input>' +
' <label for="' + scope.$id + attrs.model + '" class="cr1"></label>';
var template = '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
' ng-model="' + model + '"' + ngchange +
' ng-checked="' + model + '"></input>' +
' <label for="' + scope.$id + model + '" class="cr1"></label>';
template = label + template;
elem.replaceWith($compile(angular.element(template))(scope));

View File

@@ -57,7 +57,7 @@
<div class="editor-row">
<div class="tight-form-section">
<h5>Toggles</h5>
<div class="tight-form">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
<editor-checkbox text="Editable" model="dashboard.editable"></editor-checkbox>
@@ -65,7 +65,7 @@
<li class="tight-form-item">
<editor-checkbox text="Hide Controls (CTRL+H)" model="dashboard.hideControls"></editor-checkbox>
</li>
<li class="tight-form-item">
<li class="tight-form-item last">
<editor-checkbox text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-checkbox>
</li>
</ul>

View File

@@ -1,8 +1,9 @@
define([
'angular',
'config',
'lodash',
],
function (angular, config) {
function (angular, config, _) {
'use strict';
var module = angular.module('grafana.controllers');
@@ -12,12 +13,16 @@ function (angular, config) {
$scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html';
var defaults = {
name: '',
type: 'graphite',
url: '',
access: 'proxy'
};
var defaults = {name: '', type: 'graphite', url: '', access: 'proxy' };
$scope.indexPatternTypes = [
{name: 'No pattern', value: undefined},
{name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'},
{name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'},
{name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'},
{name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'},
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
$scope.init = function() {
$scope.isNew = true;
@@ -117,6 +122,11 @@ function (angular, config) {
}
};
$scope.indexPatternTypeChanged = function() {
var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
$scope.current.database = def.example || 'es-index-name';
};
$scope.init();
});

View File

@@ -23,9 +23,7 @@
Basic Auth
</li>
<li class="tight-form-item">
Enable&nbsp;
<input class="cr1" id="current.basicAuth" type="checkbox" ng-model="current.basicAuth" ng-checked="current.basicAuth">
<label for="current.basicAuth" class="cr1"></label>
<editor-checkbox text="Enable" model="current.basicAuth"></editor-checkbox>
</li>
<li class="tight-form-item" ng-if="current.basicAuth">
User

View File

@@ -0,0 +1,16 @@
{
"pluginType": "datasource",
"name": "CloudWatch",
"type": "cloudwatch",
"serviceName": "CloudWatchDatasource",
"module": "plugins/datasource/cloudwatch/datasource",
"partials": {
"config": "app/plugins/datasource/cloudwatch/partials/config.html",
"query": "app/plugins/datasource/cloudwatch/partials/query.editor.html"
},
"metrics": true
}

View File

@@ -0,0 +1,561 @@
/* global AWS */
define([
'angular',
'lodash',
'kbn',
'moment',
'./queryCtrl',
'./directives',
'aws-sdk',
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.factory('CloudWatchDatasource', function($q, $http, templateSrv) {
function CloudWatchDatasource(datasource) {
this.type = 'cloudwatch';
this.name = datasource.name;
this.supportMetrics = true;
this.proxyMode = (datasource.jsonData.access === 'proxy');
this.proxyUrl = datasource.url;
this.defaultRegion = datasource.jsonData.defaultRegion;
this.credentials = {
accessKeyId: datasource.jsonData.accessKeyId,
secretAccessKey: datasource.jsonData.secretAccessKey
};
/* jshint -W101 */
this.supportedRegion = [
'us-east-1', 'us-west-2', 'us-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'sa-east-1'
];
this.supportedMetrics = {
'AWS/AutoScaling': [
'GroupMinSize', 'GroupMaxSize', 'GroupDesiredCapacity', 'GroupInServiceInstances', 'GroupPendingInstances', 'GroupStandbyInstances', 'GroupTerminatingInstances', 'GroupTotalInstances'
],
'AWS/Billing': [
'EstimatedCharges'
],
'AWS/CloudFront': [
'Requests', 'BytesDownloaded', 'BytesUploaded', 'TotalErrorRate', '4xxErrorRate', '5xxErrorRate'
],
'AWS/CloudSearch': [
'SuccessfulRequests', 'SearchableDocuments', 'IndexUtilization', 'Partitions'
],
'AWS/DynamoDB': [
'ConditionalCheckFailedRequests', 'ConsumedReadCapacityUnits', 'ConsumedWriteCapacityUnits', 'OnlineIndexConsumedWriteCapacity', 'OnlineIndexPercentageProgress', 'OnlineIndexThrottleEvents', 'ProvisionedReadCapacityUnits', 'ProvisionedWriteCapacityUnits', 'ReadThrottleEvents', 'ReturnedItemCount', 'SuccessfulRequestLatency', 'SystemErrors', 'ThrottledRequests', 'UserErrors', 'WriteThrottleEvents'
],
'AWS/ElastiCache': [
'CPUUtilization', 'SwapUsage', 'FreeableMemory', 'NetworkBytesIn', 'NetworkBytesOut',
'BytesUsedForCacheItems', 'BytesReadIntoMemcached', 'BytesWrittenOutFromMemcached', 'CasBadval', 'CasHits', 'CasMisses', 'CmdFlush', 'CmdGet', 'CmdSet', 'CurrConnections', 'CurrItems', 'DecrHits', 'DecrMisses', 'DeleteHits', 'DeleteMisses', 'Evictions', 'GetHits', 'GetMisses', 'IncrHits', 'IncrMisses', 'Reclaimed',
'CurrConnections', 'Evictions', 'Reclaimed', 'NewConnections', 'BytesUsedForCache', 'CacheHits', 'CacheMisses', 'ReplicationLag', 'GetTypeCmds', 'SetTypeCmds', 'KeyBasedCmds', 'StringBasedCmds', 'HashBasedCmds', 'ListBasedCmds', 'SetBasedCmds', 'SortedSetBasedCmds', 'CurrItems'
],
'AWS/EBS': [
'VolumeReadBytes', 'VolumeWriteBytes', 'VolumeReadOps', 'VolumeWriteOps', 'VolumeTotalReadTime', 'VolumeTotalWriteTime', 'VolumeIdleTime', 'VolumeQueueLength', 'VolumeThroughputPercentage', 'VolumeConsumedReadWriteOps'
],
'AWS/EC2': [
'CPUCreditUsage', 'CPUCreditBalance', 'CPUUtilization', 'DiskReadOps', 'DiskWriteOps', 'DiskReadBytes', 'DiskWriteBytes', 'NetworkIn', 'NetworkOut', 'StatusCheckFailed', 'StatusCheckFailed_Instance', 'StatusCheckFailed_System'
],
'AWS/ELB': [
'HealthyHostCount', 'UnHealthyHostCount', 'RequestCount', 'Latency', 'HTTPCode_ELB_4XX', 'HTTPCode_ELB_5XX', 'HTTPCode_Backend_2XX', 'HTTPCode_Backend_3XX', 'HTTPCode_Backend_4XX', 'HTTPCode_Backend_5XX', 'BackendConnectionErrors', 'SurgeQueueLength', 'SpilloverCount'
],
'AWS/ElasticMapReduce': [
'CoreNodesPending', 'CoreNodesRunning', 'HBaseBackupFailed', 'HBaseMostRecentBackupDuration', 'HBaseTimeSinceLastSuccessfulBackup', 'HDFSBytesRead', 'HDFSBytesWritten', 'HDFSUtilization', 'IsIdle', 'JobsFailed', 'JobsRunning', 'LiveDataNodes', 'LiveTaskTrackers', 'MapSlotsOpen', 'MissingBlocks', 'ReduceSlotsOpen', 'RemainingMapTasks', 'RemainingMapTasksPerSlot', 'RemainingReduceTasks', 'RunningMapTasks', 'RunningReduceTasks', 'S3BytesRead', 'S3BytesWritten', 'TaskNodesPending', 'TaskNodesRunning', 'TotalLoad'
],
'AWS/Kinesis': [
'PutRecord.Bytes', 'PutRecord.Latency', 'PutRecord.Success', 'PutRecords.Bytes', 'PutRecords.Latency', 'PutRecords.Records', 'PutRecords.Success', 'IncomingBytes', 'IncomingRecords', 'GetRecords.Bytes', 'GetRecords.IteratorAgeMilliseconds', 'GetRecords.Latency', 'GetRecords.Success'
],
'AWS/ML': [
'PredictCount', 'PredictFailureCount'
],
'AWS/OpsWorks': [
'cpu_idle', 'cpu_nice', 'cpu_system', 'cpu_user', 'cpu_waitio', 'load_1', 'load_5', 'load_15', 'memory_buffers', 'memory_cached', 'memory_free', 'memory_swap', 'memory_total', 'memory_used', 'procs'
],
'AWS/Redshift': [
'CPUUtilization', 'DatabaseConnections', 'HealthStatus', 'MaintenanceMode', 'NetworkReceiveThroughput', 'NetworkTransmitThroughput', 'PercentageDiskSpaceUsed', 'ReadIOPS', 'ReadLatency', 'ReadThroughput', 'WriteIOPS', 'WriteLatency', 'WriteThroughput'
],
'AWS/RDS': [
'BinLogDiskUsage', 'CPUUtilization', 'DatabaseConnections', 'DiskQueueDepth', 'FreeableMemory', 'FreeStorageSpace', 'ReplicaLag', 'SwapUsage', 'ReadIOPS', 'WriteIOPS', 'ReadLatency', 'WriteLatency', 'ReadThroughput', 'WriteThroughput', 'NetworkReceiveThroughput', 'NetworkTransmitThroughput'
],
'AWS/Route53': [
'HealthCheckStatus', 'HealthCheckPercentageHealthy'
],
'AWS/SNS': [
'NumberOfMessagesPublished', 'PublishSize', 'NumberOfNotificationsDelivered', 'NumberOfNotificationsFailed'
],
'AWS/SQS': [
'NumberOfMessagesSent', 'SentMessageSize', 'NumberOfMessagesReceived', 'NumberOfEmptyReceives', 'NumberOfMessagesDeleted', 'ApproximateNumberOfMessagesDelayed', 'ApproximateNumberOfMessagesVisible', 'ApproximateNumberOfMessagesNotVisible'
],
'AWS/S3': [
'BucketSizeBytes', 'NumberOfObjects'
],
'AWS/SWF': [
'DecisionTaskScheduleToStartTime', 'DecisionTaskStartToCloseTime', 'DecisionTasksCompleted', 'StartedDecisionTasksTimedOutOnClose', 'WorkflowStartToCloseTime', 'WorkflowsCanceled', 'WorkflowsCompleted', 'WorkflowsContinuedAsNew', 'WorkflowsFailed', 'WorkflowsTerminated', 'WorkflowsTimedOut'
],
'AWS/StorageGateway': [
'CacheHitPercent', 'CachePercentUsed', 'CachePercentDirty', 'CloudBytesDownloaded', 'CloudDownloadLatency', 'CloudBytesUploaded', 'UploadBufferFree', 'UploadBufferPercentUsed', 'UploadBufferUsed', 'QueuedWrites', 'ReadBytes', 'ReadTime', 'TotalCacheSize', 'WriteBytes', 'WriteTime', 'WorkingStorageFree', 'WorkingStoragePercentUsed', 'WorkingStorageUsed', 'CacheHitPercent', 'CachePercentUsed', 'CachePercentDirty', 'ReadBytes', 'ReadTime', 'WriteBytes', 'WriteTime', 'QueuedWrites'
],
'AWS/WorkSpaces': [
'Available', 'Unhealthy', 'ConnectionAttempt', 'ConnectionSuccess', 'ConnectionFailure', 'SessionLaunchTime', 'InSessionLatency', 'SessionDisconnect'
],
};
this.supportedDimensions = {
'AWS/AutoScaling': [
'AutoScalingGroupName'
],
'AWS/Billing': [
'ServiceName', 'LinkedAccount', 'Currency'
],
'AWS/CloudFront': [
'DistributionId', 'Region'
],
'AWS/CloudSearch': [
],
'AWS/DynamoDB': [
'TableName', 'GlobalSecondaryIndexName', 'Operation'
],
'AWS/ElastiCache': [
'CacheClusterId', 'CacheNodeId'
],
'AWS/EBS': [
'VolumeId'
],
'AWS/EC2': [
'AutoScalingGroupName', 'ImageId', 'InstanceId', 'InstanceType'
],
'AWS/ELB': [
'LoadBalancerName', 'AvailabilityZone'
],
'AWS/ElasticMapReduce': [
'ClusterId', 'JobId'
],
'AWS/Kinesis': [
'StreamName'
],
'AWS/ML': [
'MLModelId', 'RequestMode'
],
'AWS/OpsWorks': [
'StackId', 'LayerId', 'InstanceId'
],
'AWS/Redshift': [
'NodeID', 'ClusterIdentifier'
],
'AWS/RDS': [
'DBInstanceIdentifier', 'DatabaseClass', 'EngineName'
],
'AWS/Route53': [
'HealthCheckId'
],
'AWS/SNS': [
'Application', 'Platform', 'TopicName'
],
'AWS/SQS': [
'QueueName'
],
'AWS/S3': [
'BucketName', 'StorageType'
],
'AWS/SWF': [
'Domain', 'ActivityTypeName', 'ActivityTypeVersion'
],
'AWS/StorageGateway': [
'GatewayId', 'GatewayName', 'VolumeId'
],
'AWS/WorkSpaces': [
'DirectoryId', 'WorkspaceId'
],
};
/* jshint +W101 */
/* load custom metrics definitions */
var self = this;
$q.all(
_.chain(datasource.jsonData.customMetricsAttributes)
.reject(function(u) {
return _.isEmpty(u);
})
.map(function(u) {
return $http({ method: 'GET', url: u });
})
)
.then(function(allResponse) {
_.chain(allResponse)
.map(function(d) {
return d.data.Metrics;
})
.flatten()
.reject(function(metric) {
return metric.Namespace.indexOf('AWS/') === 0;
})
.map(function(metric) {
metric.Dimensions = _.chain(metric.Dimensions)
.map(function(d) {
return d.Name;
})
.value().sort();
return metric;
})
.uniq(function(metric) {
return metric.Namespace + metric.MetricName + metric.Dimensions.join('');
})
.each(function(metric) {
if (!_.has(self.supportedMetrics, metric.Namespace)) {
self.supportedMetrics[metric.Namespace] = [];
}
self.supportedMetrics[metric.Namespace].push(metric.MetricName);
if (!_.has(self.supportedDimensions, metric.Namespace)) {
self.supportedDimensions[metric.Namespace] = [];
}
self.supportedDimensions[metric.Namespace] = _.union(self.supportedDimensions[metric.Namespace], metric.Dimensions);
});
});
}
// Called once per panel (graph)
CloudWatchDatasource.prototype.query = function(options) {
var start = convertToCloudWatchTime(options.range.from);
var end = convertToCloudWatchTime(options.range.to);
var queries = [];
_.each(options.targets, _.bind(function(target) {
if (!target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
return;
}
var query = {};
query.region = templateSrv.replace(target.region, options.scopedVars);
query.namespace = templateSrv.replace(target.namespace, options.scopedVars);
query.metricName = templateSrv.replace(target.metricName, options.scopedVars);
query.dimensions = convertDimensionFormat(target.dimensions);
query.statistics = getActivatedStatistics(target.statistics);
query.period = parseInt(target.period, 10);
var range = end - start;
// CloudWatch limit datapoints up to 1440
if (range / query.period >= 1440) {
query.period = Math.floor(range / 1440 / 60) * 60;
}
queries.push(query);
}, this));
// No valid targets, return the empty result to save a round trip.
if (_.isEmpty(queries)) {
var d = $q.defer();
d.resolve({ data: [] });
return d.promise;
}
var allQueryPromise = _.map(queries, _.bind(function(query) {
return this.performTimeSeriesQuery(query, start, end);
}, this));
return $q.all(allQueryPromise)
.then(function(allResponse) {
var result = [];
_.each(allResponse, function(response, index) {
var metrics = transformMetricData(response, options.targets[index]);
_.each(metrics, function(m) {
result.push(m);
});
});
return { data: result };
});
};
CloudWatchDatasource.prototype.performTimeSeriesQuery = function(query, start, end) {
var cloudwatch = this.getCloudWatchClient(query.region);
var params = {
Namespace: query.namespace,
MetricName: query.metricName,
Dimensions: query.dimensions,
Statistics: query.statistics,
StartTime: start,
EndTime: end,
Period: query.period
};
var d = $q.defer();
cloudwatch.getMetricStatistics(params, function(err, data) {
if (err) {
return d.reject(err);
}
return d.resolve(data);
});
return d.promise;
};
CloudWatchDatasource.prototype.performSuggestRegion = function() {
return this.supportedRegion;
};
CloudWatchDatasource.prototype.performSuggestNamespace = function() {
return _.keys(this.supportedMetrics);
};
CloudWatchDatasource.prototype.performSuggestMetrics = function(namespace) {
namespace = templateSrv.replace(namespace);
return this.supportedMetrics[namespace] || [];
};
CloudWatchDatasource.prototype.performSuggestDimensionKeys = function(namespace) {
namespace = templateSrv.replace(namespace);
return this.supportedDimensions[namespace] || [];
};
CloudWatchDatasource.prototype.performSuggestDimensionValues = function(region, namespace, metricName, dimensions) {
region = templateSrv.replace(region);
namespace = templateSrv.replace(namespace);
metricName = templateSrv.replace(metricName);
var cloudwatch = this.getCloudWatchClient(region);
var params = {
Namespace: namespace,
MetricName: metricName
};
if (!_.isEmpty(dimensions)) {
params.Dimensions = convertDimensionFormat(dimensions);
}
var d = $q.defer();
cloudwatch.listMetrics(params, function(err, data) {
if (err) {
return d.reject(err);
}
var suggestData = _.chain(data.Metrics)
.map(function(metric) {
return metric.Dimensions;
})
.reject(function(metric) {
return _.isEmpty(metric);
})
.value();
return d.resolve(suggestData);
});
return d.promise;
};
CloudWatchDatasource.prototype.getTemplateVariableNames = function() {
var variables = [];
templateSrv.fillVariableValuesForUrl(variables);
return _.map(_.keys(variables), function(k) {
return k.replace(/var-/, '$');
});
};
CloudWatchDatasource.prototype.metricFindQuery = function(query) {
var region;
var namespace;
var metricName;
var transformSuggestData = function(suggestData) {
return _.map(suggestData, function(v) {
return { text: v };
});
};
var d = $q.defer();
var regionQuery = query.match(/^region\(\)/);
if (regionQuery) {
d.resolve(transformSuggestData(this.performSuggestRegion()));
return d.promise;
}
var namespaceQuery = query.match(/^namespace\(\)/);
if (namespaceQuery) {
d.resolve(transformSuggestData(this.performSuggestNamespace()));
return d.promise;
}
var metricNameQuery = query.match(/^metrics\(([^\)]+?)\)/);
if (metricNameQuery) {
namespace = templateSrv.replace(metricNameQuery[1]);
d.resolve(transformSuggestData(this.performSuggestMetrics(namespace)));
return d.promise;
}
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)\)/);
if (dimensionKeysQuery) {
namespace = templateSrv.replace(dimensionKeysQuery[1]);
d.resolve(transformSuggestData(this.performSuggestDimensionKeys(namespace)));
return d.promise;
}
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?([^)]*))?\)/);
if (dimensionValuesQuery) {
region = templateSrv.replace(dimensionValuesQuery[1]);
namespace = templateSrv.replace(dimensionValuesQuery[2]);
metricName = templateSrv.replace(dimensionValuesQuery[3]);
var dimensionPart = templateSrv.replace(dimensionValuesQuery[5]);
var dimensions = {};
if (!_.isEmpty(dimensionPart)) {
_.each(dimensionPart.split(','), function(v) {
var t = v.split('=');
if (t.length !== 2) {
throw new Error('Invalid query format');
}
dimensions[t[0]] = t[1];
});
}
return this.performSuggestDimensionValues(region, namespace, metricName, dimensions)
.then(function(suggestData) {
return _.map(suggestData, function(dimensions) {
var result = _.chain(dimensions)
.sortBy(function(dimension) {
return dimension.Name;
})
.map(function(dimension) {
return dimension.Name + '=' + dimension.Value;
})
.value().join(',');
return { text: result };
});
});
}
return $q.when([]);
};
CloudWatchDatasource.prototype.testDatasource = function() {
/* use billing metrics for test */
var region = 'us-east-1';
var namespace = 'AWS/Billing';
var metricName = 'EstimatedCharges';
var dimensions = {};
return this.performSuggestDimensionValues(region, namespace, metricName, dimensions).then(function () {
return { status: 'success', message: 'Data source is working', title: 'Success' };
});
};
CloudWatchDatasource.prototype.getCloudWatchClient = function(region) {
if (!this.proxyMode) {
return new AWS.CloudWatch({
region: region,
accessKeyId: this.credentials.accessKeyId,
secretAccessKey: this.credentials.secretAccessKey
});
} else {
var self = this;
var generateRequestProxy = function(service, action) {
return function(params, callback) {
var data = {
region: region,
service: service,
action: action,
parameters: params
};
var options = {
method: 'POST',
url: self.proxyUrl,
data: data
};
$http(options).then(function(response) {
callback(null, response.data);
}, function(err) {
callback(err, []);
});
};
};
return {
getMetricStatistics: generateRequestProxy('CloudWatch', 'GetMetricStatistics'),
listMetrics: generateRequestProxy('CloudWatch', 'ListMetrics')
};
}
};
CloudWatchDatasource.prototype.getDefaultRegion = function() {
return this.defaultRegion;
};
function transformMetricData(md, options) {
var result = [];
var dimensionPart = templateSrv.replace(JSON.stringify(options.dimensions));
_.each(getActivatedStatistics(options.statistics), function(s) {
var originalSettings = _.templateSettings;
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var template = _.template(options.legendFormat);
var metricLabel;
if (_.isEmpty(options.legendFormat)) {
metricLabel = md.Label + '_' + s + dimensionPart;
} else {
var d = convertDimensionFormat(options.dimensions);
metricLabel = template({
Region: templateSrv.replace(options.region),
Namespace: templateSrv.replace(options.namespace),
MetricName: templateSrv.replace(options.metricName),
Dimensions: d,
Statistics: s
});
}
_.templateSettings = originalSettings;
var dps = _.map(md.Datapoints, function(value) {
return [value[s], new Date(value.Timestamp).getTime()];
});
dps = _.sortBy(dps, function(dp) { return dp[1]; });
result.push({ target: metricLabel, datapoints: dps });
});
return result;
}
function getActivatedStatistics(statistics) {
var activatedStatistics = [];
_.each(statistics, function(v, k) {
if (v) {
activatedStatistics.push(k);
}
});
return activatedStatistics;
}
function convertToCloudWatchTime(date) {
return Math.round(kbn.parseDate(date).getTime() / 1000);
}
function convertDimensionFormat(dimensions) {
return _.map(_.keys(dimensions), function(key) {
return {
Name: templateSrv.replace(key),
Value: templateSrv.replace(dimensions[key])
};
});
}
return CloudWatchDatasource;
});
});

View File

@@ -0,0 +1,13 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorCloudwatch', function() {
return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
});
});

View File

@@ -0,0 +1,54 @@
<h5>CloudWatch details</h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Default Region
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.defaultRegion' placeholder="" required></input>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item">
Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</label>
</li>
<li>
<select class="input-small tight-form-input" ng-model="current.jsonData.access" ng-options="f for f in ['direct', 'proxy']" ng-init="current.jsonData.access = current.jsonData.access || 'direct'"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-show="current.jsonData.access === 'direct'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Access Key Id
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.accessKeyId' placeholder="" ng-required="current.jsonData.access === 'direct'"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Secret Access Key
</li>
<li>
<input type="password" class="tight-form-input input-xlarge" ng-model='current.jsonData.secretAccessKey' placeholder="" ng-required="current.jsonData.access === 'direct'"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Custom Metric Attributes
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.customMetricsAttributes[0]' ng-init="current.jsonData.customMetricsAttributes = current.jsonData.customMetricsAttributes || []" placeholder="JSON url" bs-tooltip="'Set JSON url of the result, \'aws cloudwatch list-metrics --output json\''"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,153 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 100px">
Metric
</li>
<li>
<metric-segment segment="regionSegment" get-alt-segments="getRegions()" on-value-changed="regionChanged()"></metric-segment>
</li>
<li>
<metric-segment segment="namespaceSegment" get-alt-segments="getNamespaces()" on-value-changed="namespaceChanged()"></metric-segment>
</li>
<li>
<metric-segment segment="metricSegment" get-alt-segments="getMetrics()" on-value-changed="metricChanged()"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item tight-form-align" style="width: 100px">
Dimensions
</li>
<li ng-repeat="(key, value) in target.escapedDimensions track by $index" class="tight-form-item">
{{key}}&nbsp;=&nbsp;{{value}}
<a ng-click="removeDimension(key)">
<i class="fa fa-remove"></i>
</a>
</li>
<li class="tight-form-item" ng-hide="addDimensionMode">
<a ng-click="addDimension()">
<i class="fa fa-plus"></i>
</a>
</li>
<li ng-show="addDimensionMode">
<input type="text"
class="input-small tight-form-input"
spellcheck='false'
bs-typeahead="suggestDimensionKeys"
data-min-length=0 data-items=100
ng-model="target.currentDimensionKey"
placeholder="key">
<input type="text"
class="input-small tight-form-input"
spellcheck='false'
bs-typeahead="suggestDimensionValues"
data-min-length=0 data-items=100
ng-model="target.currentDimensionValue"
placeholder="value">
<a ng-click="addDimension()">
add dimension
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item tight-form-align" style="width: 100px">
Statistics
</li>
<li class="tight-form-item">
<editor-checkbox text="Min" model="target.statistics.Minimum" change="statisticsOptionChanged()"></editor-checkbox>
<editor-checkbox text="Max" model="target.statistics.Maximum" change="statisticsOptionChanged()"></editor-checkbox>
<editor-checkbox text="Avg" model="target.statistics.Average" change="statisticsOptionChanged()"></editor-checkbox>
<editor-checkbox text="Sum" model="target.statistics.Sum" change="statisticsOptionChanged()"></editor-checkbox>
<editor-checkbox text="SampleCount" model="target.statistics.SampleCount" change="statisticsOptionChanged()"></editor-checkbox>
</li>
<li class="tight-form-item">
Period
</li>
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="target.period"
data-placement="right"
spellcheck='false'
placeholder="period"
data-min-length=0 data-items=100
ng-model-onblur
ng-change="refreshMetricData()"
/>
<a bs-tooltip="target.errors.period"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.period">
<i class="fa fa-warning"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item tight-form-align" style="width: 100px">
Alias Pattern
</li>
<li>
<input type="text"
class="input-xxlarge tight-form-input"
ng-model="target.legendFormat"
spellcheck='false'
placeholder="Syntax: {{Region}} {{Namespace}} {{MetricName}} {{Statistics}} {{Dimensions[N].Name}} {{Dimensions[N].Value}}"
data-min-length=0 data-items=100
ng-model-onblur
ng-change="refreshMetricData()"
>
<tip>Syntax: {{Region}} {{Namespace}} {{MetricName}} {{Statistics}} {{Dimensions[N].Name}} {{Dimensions[N].Value}}</tip>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,172 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('CloudWatchQueryCtrl', function($scope, templateSrv, uiSegmentSrv) {
$scope.init = function() {
$scope.target.namespace = $scope.target.namespace || '';
$scope.target.metricName = $scope.target.metricName || '';
$scope.target.dimensions = $scope.target.dimensions || {};
$scope.target.escapedDimensions = this.escapeDimensions($scope.target.dimensions);
$scope.target.statistics = $scope.target.statistics || {};
$scope.target.period = $scope.target.period || 60;
$scope.target.region = $scope.target.region || $scope.datasource.getDefaultRegion();
$scope.target.errors = validateTarget();
$scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
$scope.namespaceSegment = uiSegmentSrv.getSegmentForValue($scope.target.namespace, 'select namespace');
$scope.metricSegment = uiSegmentSrv.getSegmentForValue($scope.target.metricName, 'select metric');
};
$scope.getRegions = function() {
return $scope.datasource.metricFindQuery('region()')
.then($scope.transformToSegments(true));
};
$scope.getNamespaces = function() {
return $scope.datasource.metricFindQuery('namespace()')
.then($scope.transformToSegments(true));
};
$scope.getMetrics = function() {
return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')')
.then($scope.transformToSegments(true));
};
$scope.regionChanged = function() {
$scope.target.region = $scope.regionSegment.value;
$scope.get_data();
};
$scope.namespaceChanged = function() {
$scope.target.namespace = $scope.namespaceSegment.value;
$scope.get_data();
};
$scope.metricChanged = function() {
$scope.target.metricName = $scope.metricSegment.value;
$scope.get_data();
};
$scope.transformToSegments = function(addTemplateVars) {
return function(results) {
var segments = _.map(results, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
});
}
return segments;
};
};
$scope.refreshMetricData = function() {
$scope.target.errors = validateTarget($scope.target);
// this does not work so good
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
$scope.suggestDimensionKeys = function(query, callback) { // jshint unused:false
return _.union($scope.datasource.performSuggestDimensionKeys($scope.target.namespace), $scope.datasource.getTemplateVariableNames());
};
$scope.suggestDimensionValues = function(query, callback) {
if (!$scope.target.namespace || !$scope.target.metricName) {
return callback([]);
}
$scope.datasource.performSuggestDimensionValues(
$scope.target.region,
$scope.target.namespace,
$scope.target.metricName,
$scope.target.dimensions
)
.then(function(result) {
var suggestData = _.chain(result)
.flatten(true)
.filter(function(dimension) {
return dimension.Name === templateSrv.replace($scope.target.currentDimensionKey);
})
.pluck('Value')
.uniq()
.value();
suggestData = _.union(suggestData, $scope.datasource.getTemplateVariableNames());
callback(suggestData);
}, function() {
callback([]);
});
};
$scope.addDimension = function() {
if (!$scope.addDimensionMode) {
$scope.addDimensionMode = true;
return;
}
if (!$scope.target.dimensions) {
$scope.target.dimensions = {};
}
$scope.target.dimensions[$scope.target.currentDimensionKey] = $scope.target.currentDimensionValue;
$scope.target.escapedDimensions = this.escapeDimensions($scope.target.dimensions);
$scope.target.currentDimensionKey = '';
$scope.target.currentDimensionValue = '';
$scope.refreshMetricData();
$scope.addDimensionMode = false;
};
$scope.removeDimension = function(key) {
key = key.replace(/\\\$/g, '$');
delete $scope.target.dimensions[key];
$scope.target.escapedDimensions = this.escapeDimensions($scope.target.dimensions);
$scope.refreshMetricData();
};
$scope.escapeDimensions = function(d) {
var result = {};
_.chain(d)
.keys(d)
.each(function(k) {
var v = d[k];
result[k.replace(/\$/g, '\uFF04')] = v.replace(/\$/g, '\$');
});
return result;
};
$scope.statisticsOptionChanged = function() {
$scope.refreshMetricData();
};
// TODO: validate target
function validateTarget() {
var errs = {};
if ($scope.target.period < 60 || ($scope.target.period % 60) !== 0) {
errs.period = 'Period must be at least 60 seconds and must be a multiple of 60';
}
return errs;
}
$scope.init();
});
});

View File

@@ -0,0 +1,115 @@
define([
'angular',
'lodash',
'./queryDef',
],
function (angular, _, queryDef) {
'use strict';
var module = angular.module('grafana.directives');
module.controller('ElasticBucketAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
var bucketAggs = $scope.target.bucketAggs;
$scope.orderByOptions = [];
$scope.bucketAggTypes = queryDef.bucketAggTypes;
$scope.orderOptions = queryDef.orderOptions;
$scope.sizeOptions = queryDef.sizeOptions;
$scope.intervalOptions = queryDef.intervalOptions;
$rootScope.onAppEvent('elastic-query-updated', function() {
$scope.validateModel();
$scope.updateOrderByOptions();
});
$scope.init = function() {
$scope.agg = bucketAggs[$scope.index];
$scope.validateModel();
};
$scope.onChangeInternal = function() {
$scope.onChange();
};
$scope.onTypeChanged = function() {
$scope.agg.settings = {};
$scope.showOptions = false;
$scope.validateModel();
$scope.onChange();
};
$scope.validateModel = function() {
$scope.index = _.indexOf(bucketAggs, $scope.agg);
$scope.isFirst = $scope.index === 0;
$scope.isLast = $scope.index === bucketAggs.length - 1;
var settingsLinkText = "";
var settings = $scope.agg.settings || {};
switch($scope.agg.type) {
case 'terms': {
settings.order = settings.order || "asc";
settings.size = settings.size || "0";
settings.orderBy = settings.orderBy || "_term";
if (settings.size !== '0') {
settingsLinkText = queryDef.describeOrder(settings.order) + ' ' + settings.size + ', ';
}
settingsLinkText += 'Order by: ' + queryDef.describeOrderBy(settings.orderBy, $scope.target);
if (settings.size === '0') {
settingsLinkText += ' (' + settings.order + ')';
}
break;
}
case 'date_histogram': {
settings.interval = settings.interval || 'auto';
$scope.agg.field = $scope.target.timeField;
settingsLinkText = 'Interval: ' + settings.interval;
}
}
$scope.settingsLinkText = settingsLinkText;
$scope.agg.settings = settings;
return true;
};
$scope.toggleOptions = function() {
$scope.showOptions = !$scope.showOptions;
$scope.updateOrderByOptions();
};
$scope.updateOrderByOptions = function() {
$scope.orderByOptions = queryDef.getOrderByOptions($scope.target);
};
$scope.addBucketAgg = function() {
// if last is date histogram add it before
var lastBucket = bucketAggs[bucketAggs.length - 1];
var addIndex = bucketAggs.length - 1;
if (lastBucket && lastBucket.type === 'date_histogram') {
addIndex - 1;
}
var id = _.reduce($scope.target.bucketAggs.concat($scope.target.metrics), function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
}, 0);
bucketAggs.splice(addIndex, 0, {type: "terms", field: "select field", id: (id+1).toString()});
$scope.onChange();
};
$scope.removeBucketAgg = function() {
bucketAggs.splice($scope.index, 1);
$scope.onChange();
};
$scope.init();
});
});

View File

@@ -1,12 +1,15 @@
define([
'angular',
'lodash',
'config',
'kbn',
'moment',
'kbn',
'./queryBuilder',
'./indexPattern',
'./elasticResponse',
'./queryCtrl',
'./directives'
],
function (angular, _, config, kbn, moment) {
function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) {
'use strict';
var module = angular.module('grafana.services');
@@ -19,15 +22,16 @@ function (angular, _, config, kbn, moment) {
this.url = datasource.url;
this.name = datasource.name;
this.index = datasource.index;
this.searchMaxResults = config.search.max_results || 20;
this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
this.timeField = datasource.jsonData.timeField;
this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval);
this.queryBuilder = new ElasticQueryBuilder({
timeField: this.timeField
});
}
ElasticDatasource.prototype._request = function(method, url, index, data) {
ElasticDatasource.prototype._request = function(method, url, data) {
var options = {
url: this.url + "/" + index + url,
url: this.url + "/" + url,
method: method,
data: data
};
@@ -43,14 +47,14 @@ function (angular, _, config, kbn, moment) {
};
ElasticDatasource.prototype._get = function(url) {
return this._request('GET', url, this.index)
return this._request('GET', this.indexPattern.getIndexForToday() + url)
.then(function(results) {
return results.data;
});
};
ElasticDatasource.prototype._post = function(url, data) {
return this._request('POST', url, this.index, data)
return this._request('POST', url, data)
.then(function(results) {
return results.data;
});
@@ -78,7 +82,7 @@ function (angular, _, config, kbn, moment) {
"size": 10000
};
return this._request('POST', '/_search', annotation.index, data).then(function(results) {
return this._request('POST', annotation.index + '/_search', data).then(function(results) {
var list = [];
var hits = results.data.hits.hits;
@@ -125,175 +129,92 @@ function (angular, _, config, kbn, moment) {
});
};
ElasticDatasource.prototype._getDashboardWithSlug = function(id) {
return this._get('/dashboard/' + kbn.slugifyForUrl(id))
.then(function(result) {
return angular.fromJson(result._source.dashboard);
}, function() {
throw "Dashboard not found";
});
};
ElasticDatasource.prototype.getDashboard = function(id, isTemp) {
var url = '/dashboard/' + id;
if (isTemp) { url = '/temp/' + id; }
var self = this;
return this._get(url)
.then(function(result) {
return angular.fromJson(result._source.dashboard);
}, function(data) {
if(data.status === 0) {
throw "Could not contact Elasticsearch. Please ensure that Elasticsearch is reachable from your browser.";
} else {
// backward compatible fallback
return self._getDashboardWithSlug(id);
}
});
};
ElasticDatasource.prototype.saveDashboard = function(dashboard) {
var title = dashboard.title;
var temp = dashboard.temp;
if (temp) { delete dashboard.temp; }
var data = {
user: 'guest',
group: 'guest',
title: title,
tags: dashboard.tags,
dashboard: angular.toJson(dashboard)
};
if (temp) {
return this._saveTempDashboard(data);
}
else {
var id = encodeURIComponent(kbn.slugifyForUrl(title));
var self = this;
return this._request('PUT', '/dashboard/' + id, this.index, data)
.then(function(results) {
self._removeUnslugifiedDashboard(results, title, id);
return { title: title, url: '/dashboard/db/' + id };
}, function() {
throw 'Failed to save to elasticsearch';
});
}
};
ElasticDatasource.prototype._removeUnslugifiedDashboard = function(saveResult, title, id) {
if (saveResult.statusText !== 'Created') { return; }
if (title === id) { return; }
var self = this;
this._get('/dashboard/' + title).then(function() {
self.deleteDashboard(title);
ElasticDatasource.prototype.testDatasource = function() {
return this._get('/_stats').then(function() {
return { status: "success", message: "Data source is working", title: "Success" };
}, function(err) {
if (err.data && err.data.error) {
return { status: "error", message: err.data.error, title: "Error" };
} else {
return { status: "error", message: err.status, title: "Error" };
}
});
};
ElasticDatasource.prototype._saveTempDashboard = function(data) {
return this._request('POST', '/temp/?ttl=' + this.saveTempTTL, this.index, data)
.then(function(result) {
var baseUrl = window.location.href.replace(window.location.hash,'');
var url = baseUrl + "#dashboard/temp/" + result.data._id;
return { title: data.title, url: url };
}, function(err) {
throw "Failed to save to temp dashboard to elasticsearch " + err.data;
});
ElasticDatasource.prototype.getQueryHeader = function(timeRange) {
var header = {search_type: "count", "ignore_unavailable": true};
var from = kbn.parseDate(timeRange.from);
var to = kbn.parseDate(timeRange.to);
header.index = this.indexPattern.getIndexList(from, to);
return angular.toJson(header);
};
ElasticDatasource.prototype.deleteDashboard = function(id) {
return this._request('DELETE', '/dashboard/' + id, this.index)
.then(function(result) {
return result.data._id;
}, function(err) {
throw err.data;
});
ElasticDatasource.prototype.query = function(options) {
var payload = "";
var target;
var sentTargets = [];
var header = this.getQueryHeader(options.range);
var timeFrom = this.translateTime(options.range.from);
var timeTo = this.translateTime(options.range.to);
for (var i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (target.hide) {return;}
var esQuery = this.queryBuilder.build(target, timeFrom, timeTo);
payload += header + '\n';
payload += angular.toJson(esQuery) + '\n';
sentTargets.push(target);
}
payload = payload.replace(/\$interval/g, options.interval);
payload = payload.replace(/\$timeFrom/g, this.translateTime(options.range.from));
payload = payload.replace(/\$timeTo/g, this.translateTime(options.range.to));
payload = payload.replace(/\$maxDataPoints/g, options.maxDataPoints);
payload = templateSrv.replace(payload, options.scopedVars);
return this._post('/_msearch?search_type=count', payload).then(function(res) {
return new ElasticResponse(sentTargets, res).getTimeSeries();
});
};
ElasticDatasource.prototype.searchDashboards = function(queryString) {
var endsInOpen = function(string, opener, closer) {
var character;
var count = 0;
for (var i = 0, len = string.length; i < len; i++) {
character = string[i];
if (character === opener) {
count++;
} else if (character === closer) {
count--;
}
}
return count > 0;
};
var tagsOnly = queryString.indexOf('tags!:') === 0;
if (tagsOnly) {
var tagsQuery = queryString.substring(6, queryString.length);
queryString = 'tags:' + tagsQuery + '*';
}
else {
if (queryString.length === 0) {
queryString = 'title:';
}
// make this a partial search if we're not in some reserved portion of the language, comments on conditionals, in order:
// 1. ends in reserved character, boosting, boolean operator ( -foo)
// 2. typing a reserved word like AND, OR, NOT
// 3. open parens (groupiing)
// 4. open " (term phrase)
// 5. open [ (range)
// 6. open { (range)
// see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
if (!queryString.match(/(\*|\]|}|~|\)|"|^\d+|\s[\-+]\w+)$/) &&
!queryString.match(/[A-Z]$/) &&
!endsInOpen(queryString, '(', ')') &&
!endsInOpen(queryString, '"', '"') &&
!endsInOpen(queryString, '[', ']') && !endsInOpen(queryString, '[', '}') &&
!endsInOpen(queryString, '{', ']') && !endsInOpen(queryString, '{', '}')
){
queryString += '*';
}
ElasticDatasource.prototype.translateTime = function(date) {
if (_.isString(date)) {
return date;
}
var query = {
query: { query_string: { query: queryString } },
facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } },
size: 10000,
sort: ["_uid"],
};
return date.getTime();
};
return this._post('/dashboard/_search', query)
.then(function(results) {
if(_.isUndefined(results.hits)) {
return { dashboards: [], tags: [] };
ElasticDatasource.prototype.metricFindQuery = function() {
return this._get('/_mapping').then(function(res) {
var fields = {};
for (var indexName in res) {
var index = res[indexName];
var mappings = index.mappings;
if (!mappings) { continue; }
for (var typeName in mappings) {
var properties = mappings[typeName].properties;
for (var field in properties) {
var prop = properties[field];
if (prop.type && field[0] !== '_') {
fields[field] = prop;
}
}
}
}
var resultsHits = results.hits.hits;
var displayHits = { dashboards: [], tags: results.facets.tags.terms || [] };
for (var i = 0, len = resultsHits.length; i < len; i++) {
var hit = resultsHits[i];
displayHits.dashboards.push({
id: hit._id,
title: hit._source.title,
tags: hit._source.tags
});
}
displayHits.tagsOnly = tagsOnly;
return displayHits;
fields = _.map(_.keys(fields), function(field) {
return {text: field};
});
return fields;
});
};
return ElasticDatasource;
});
});

View File

@@ -1,13 +1,51 @@
define([
'angular',
'./bucketAgg',
'./metricAgg',
],
function (angular) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorElasticsearch', function() {
return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
});
module.directive('metricQueryOptionsElasticsearch', function() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
});
module.directive('annotationsQueryEditorElasticsearch', function() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
});
module.directive('elasticMetricAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metricAgg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
module.directive('elasticBucketAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucketAgg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
});

View File

@@ -0,0 +1,187 @@
define([
"lodash",
"./queryDef"
],
function (_, queryDef) {
'use strict';
function ElasticResponse(targets, response) {
this.targets = targets;
this.response = response;
}
// This is quite complex
// neeed to recurise down the nested buckets to build series
ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, level, props) {
var value, metric, i, y, bucket, aggDef, esAgg, newSeries;
aggDef = target.bucketAggs[level];
esAgg = aggs[aggDef.id];
if (level < target.bucketAggs.length - 1) {
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
props = _.clone(props);
props[aggDef.field] = bucket.key;
this.processBuckets(bucket, target, seriesList, level+1, props);
}
return;
}
for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y];
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};
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};
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].value;
newSeries.datapoints.push([value, bucket.key]);
}
seriesList.push(newSeries);
break;
}
}
}
};
ElasticResponse.prototype._getMetricName = function(metric) {
var metricDef = _.findWhere(queryDef.metricAggTypes, {value: metric});
if (!metricDef) {
metricDef = _.findWhere(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]) { return series.props[group]; }
if (group === 'metric') { return metricName; }
if (group === 'field') { return series.field; }
return match;
});
}
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(_.pluck(seriesList, 'metric')).length;
var fieldNameCount = _.uniq(_.pluck(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.getTimeSeries = function() {
var seriesList = [];
for (var i = 0; i < this.response.responses.length; i++) {
var response = this.response.responses[i];
if (response.error) {
throw { message: response.error };
}
var aggregations = response.aggregations;
var target = this.targets[i];
var tmpSeriesList = [];
this.processBuckets(aggregations, target, tmpSeriesList, 0, {});
this.nameSeries(tmpSeriesList, target);
for (var y = 0; y < tmpSeriesList.length; y++) {
seriesList.push(tmpSeriesList[y]);
}
}
return { data: seriesList };
};
return ElasticResponse;
});

View File

@@ -0,0 +1,48 @@
define([
'lodash',
'moment',
],
function (_, moment) {
'use strict';
function IndexPattern(pattern, interval) {
this.pattern = pattern;
this.interval = interval;
}
IndexPattern.intervalMap = {
"Hours": { startOf: 'hour', amount: 'hours'},
"Daily": { startOf: 'day', amount: 'days'},
"Weekly": { startOf: 'isoWeek', amount: 'weeks'},
"Monthly": { startOf: 'month', amount: 'months'},
"Yearly": { startOf: 'year', amount: 'years'},
};
IndexPattern.prototype.getIndexForToday = function() {
if (this.interval) {
return moment().format(this.pattern);
} else {
return this.pattern;
}
};
IndexPattern.prototype.getIndexList = function(from, to) {
if (!this.interval) {
return this.pattern;
}
var intervalInfo = IndexPattern.intervalMap[this.interval];
var start = moment(from).utc().startOf(intervalInfo.startOf);
var end = moment(to).utc().startOf(intervalInfo.startOf).valueOf();
var indexList = [];
while (start <= end) {
indexList.push(start.format(this.pattern));
start.add(1, intervalInfo.amount);
}
return indexList;
};
return IndexPattern;
});

View File

@@ -0,0 +1,93 @@
define([
'angular',
'lodash',
'./queryDef'
],
function (angular, _, queryDef) {
'use strict';
var module = angular.module('grafana.directives');
module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
var metricAggs = $scope.target.metrics;
$scope.metricAggTypes = queryDef.metricAggTypes;
$scope.extendedStats = queryDef.extendedStats;
$scope.init = function() {
$scope.agg = metricAggs[$scope.index];
$scope.validateModel();
};
$rootScope.onAppEvent('elastic-query-updated', function() {
$scope.index = _.indexOf(metricAggs, $scope.agg);
$scope.validateModel();
});
$scope.validateModel = function() {
$scope.isFirst = $scope.index === 0;
$scope.isSingle = metricAggs.length === 1;
$scope.settingsLinkText = '';
if (!$scope.agg.field) {
$scope.agg.field = 'select field';
}
switch($scope.agg.type) {
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': {
var stats = _.reduce($scope.agg.meta, function(memo, val, key) {
if (val) {
var def = _.findWhere($scope.extendedStats, {value: key});
memo.push(def.text);
}
return memo;
}, []);
$scope.settingsLinkText = 'Stats: ' + stats.join(', ');
if (stats.length === 0) {
$scope.agg.meta.std_deviation_bounds_lower = true;
$scope.agg.meta.std_deviation_bounds_upper = true;
}
}
}
};
$scope.toggleOptions = function() {
$scope.showOptions = !$scope.showOptions;
};
$scope.onTypeChange = function() {
$scope.agg.settings = {};
$scope.agg.meta = {};
$scope.showOptions = false;
$scope.validateModel();
$scope.onChange();
};
$scope.addMetricAgg = function() {
var addIndex = metricAggs.length;
var id = _.reduce($scope.target.bucketAggs.concat($scope.target.metrics), function(max, val) {
return parseInt(val.id) > max ? parseInt(val.id) : max;
}, 0);
metricAggs.splice(addIndex, 0, {type: "count", field: "select field", id: (id+1).toString()});
$scope.onChange();
};
$scope.removeMetricAgg = function() {
metricAggs.splice($scope.index, 1);
$scope.onChange();
};
$scope.init();
});
});

View File

@@ -0,0 +1,80 @@
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="isFirst">Group by</span>
<span ng-hide="isFirst">Then by</span>
</li>
<li>
<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
</li>
<li>
<metric-segment-model property="agg.field" get-options="getFields()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
</li>
<li class="tight-form-item last" ng-if="settingsLinkText">
<a ng-click="toggleOptions()">{{settingsLinkText}}</a>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-if="isFirst">
<a class="pointer" ng-click="addBucketAgg()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-if="!isLast">
<a class="pointer" ng-click="removeBucketAgg()"><i class="fa fa-minus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="showOptions">
<div class="tight-form-inner-box" ng-if="agg.type === 'date_histogram'">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 60px">
Interval
</li>
<li>
<metric-segment-model property="agg.settings.interval" options="intervalOptions" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form-inner-box" ng-if="agg.type === 'terms'">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 60px">
Order
</li>
<li>
<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 60px">
Size
</li>
<li>
<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 60px">
Order By
</li>
<li>
<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@@ -1,18 +1,33 @@
<div ng-include="httpConfigPartialSrc"></div>
<br>
<h5>Elastic search details</h5>
<div class="tight-form last">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<li class="tight-form-item" style="width: 144px">
Index name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.database' placeholder="" required></input>
</li>
<li class="tight-form-item">
Pattern
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.interval" ng-options="f.value as f.name for f in indexPatternTypes" ng-change="indexPatternTypeChanged()" ></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 144px">
Time field name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.timeField' placeholder="" required ng-init="current.jsonData.timeField = current.jsonData.timeField || '@timestamp'"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,66 @@
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
Metric
</li>
<li>
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
</li>
<li ng-if="agg.type !== 'count'">
<metric-segment-model property="agg.field" get-options="getFields()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
</li>
<li class="tight-form-item last" ng-if="settingsLinkText">
<a ng-click="toggleOptions()">{{settingsLinkText}}</a>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-if="isFirst">
<a class="pointer" ng-click="addMetricAgg()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-if="!isSingle">
<a class="pointer" ng-click="removeMetricAgg()"><i class="fa fa-minus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="showOptions">
<div class="tight-form-inner-box">
<div class="tight-form last" ng-if="agg.type === 'percentiles'">
<ul class="tight-form-list">
<li class="tight-form-item">
Percentiles
</li>
<li>
<input type="text" class="input-xlarge tight-form-input last" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div ng-if="agg.type === 'extended_stats'">
<div class="tight-form" ng-repeat="stat in extendedStats">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
{{stat.text}}
</li>
<li class="tight-form-item last">
<editor-checkbox text="" model="agg.meta.{{stat.value}}" change="onChange()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form last" ng-if="agg.type === 'extended_stats'">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
Sigma
</li>
<li>
<input type="number" class="input-mini tight-form-input last" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,81 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-hide="target.rawQuery">
<li class="tight-form-item query-keyword" style="width: 75px">
Query
</li>
<li>
<input type="text" class="tight-form-input" style="width: 345px;" ng-model="target.query" spellcheck='false' placeholder="Lucence query" ng-blur="get_data()">
</li>
<li class="tight-form-item query-keyword">
Alias
</li>
<li>
<input type="text" class="tight-form-input" style="width: 260px;" ng-model="target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="get_data()">
</li>
</ul>
<div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery">
<textarea ng-model="target.rawQuery" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="queryUpdated()"></textarea>
</div>
</div>
<div ng-hide="target.rawQuery">
<div ng-repeat="agg in target.metrics">
<elastic-metric-agg
target="target" index="$index"
get-fields="getFields()"
on-change="queryUpdated()">
</elastic-metric-agg>
</div>
<div ng-repeat="agg in target.bucketAggs">
<elastic-bucket-agg
target="target" index="$index"
get-fields="getFields()"
on-change="queryUpdated()">
</elastic-bucket-agg>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<section class="grafana-metric-options">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item tight-form-item-icon">
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</section>
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
<h5>Alias patterns</h5>
<ul ng-non-bindable>
<li>{{term fieldname}} = replaced with value of term group by</li>
<li>{{metric}} = replaced with metric name (ex. Average, Min, Max)</li>
<li>{{field}} = replaced with the metric field name</li>
</ul>
</div>
</div>
</div>

View File

@@ -12,5 +12,6 @@
"annotations": "app/plugins/datasource/elasticsearch/partials/annotations.editor.html"
},
"annotations": true
"annotations": true,
"metrics": true
}

View File

@@ -0,0 +1,134 @@
define([
"angular"
],
function (angular) {
'use strict';
function ElasticQueryBuilder(options) {
this.timeField = options.timeField;
}
ElasticQueryBuilder.prototype.getRangeFilter = function() {
var filter = {};
filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"};
return filter;
};
ElasticQueryBuilder.prototype.buildTermsAgg = function(aggDef, queryNode, target) {
var metricRef, metric, size, y;
queryNode.terms = { "field": aggDef.field };
if (!aggDef.settings) {
return queryNode;
}
size = parseInt(aggDef.settings.size, 10);
if (size > 0) { queryNode.terms.size = size; }
if (aggDef.settings.orderBy !== void 0) {
queryNode.terms.order = {};
queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order;
// if metric ref, look it up and add it to this agg level
metricRef = parseInt(aggDef.settings.orderBy, 10);
if (!isNaN(metricRef)) {
for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y];
if (metric.id === aggDef.settings.orderBy) {
queryNode.aggs = {};
queryNode.aggs[metric.id] = {};
queryNode.aggs[metric.id][metric.type] = {field: metric.field};
break;
}
}
}
}
return queryNode;
};
ElasticQueryBuilder.prototype.getInterval = function(agg) {
if (agg.settings && agg.settings.interval !== 'auto') {
return agg.settings.interval;
} else {
return '$interval';
}
};
ElasticQueryBuilder.prototype.build = function(target) {
if (target.rawQuery) {
return angular.fromJson(target.rawQuery);
}
var i, nestedAggs, metric;
var query = {
"size": 0,
"query": {
"filtered": {
"query": {
"query_string": {
"analyze_wildcard": true,
"query": target.query || '*' ,
}
},
"filter": {
"bool": {
"must": [{"range": this.getRangeFilter()}]
}
}
}
}
};
nestedAggs = query;
for (i = 0; i < target.bucketAggs.length; i++) {
var aggDef = target.bucketAggs[i];
var esAgg = {};
switch(aggDef.type) {
case 'date_histogram': {
esAgg["date_histogram"] = {
"interval": this.getInterval(aggDef),
"field": this.timeField,
"min_doc_count": 1,
"extended_bounds": { "min": "$timeFrom", "max": "$timeTo" }
};
break;
}
case 'terms': {
this.buildTermsAgg(aggDef, esAgg, target);
break;
}
}
nestedAggs.aggs = nestedAggs.aggs || {};
nestedAggs.aggs[aggDef.id] = esAgg;
nestedAggs = esAgg;
}
nestedAggs.aggs = {};
for (i = 0; i < target.metrics.length; i++) {
metric = target.metrics[i];
if (metric.type === 'count') {
continue;
}
var metricAgg = {field: metric.field};
for (var prop in metric.settings) {
if (metric.settings.hasOwnProperty(prop) && metric.settings[prop] !== null) {
metricAgg[prop] = metric.settings[prop];
}
}
var aggField = {};
aggField[metric.type] = metricAgg;
nestedAggs.aggs[metric.id] = aggField;
}
return query;
};
return ElasticQueryBuilder;
});

View File

@@ -0,0 +1,69 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv, templateSrv) {
$scope.init = function() {
var target = $scope.target;
if (!target) { return; }
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
target.timeField = $scope.datasource.timeField;
};
$scope.getFields = function() {
return $scope.datasource.metricFindQuery('fields()')
.then($scope.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
$scope.queryUpdated = function() {
var newJson = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
if (newJson !== $scope.oldQueryRaw) {
$scope.rawQueryOld = newJson;
$scope.get_data();
}
$scope.appEvent('elastic-query-updated');
};
$scope.transformToSegments = function(addTemplateVars) {
return function(results) {
var segments = _.map(results, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
});
}
return segments;
};
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
};
$scope.toggleQueryMode = function () {
if ($scope.target.rawQuery) {
delete $scope.target.rawQuery;
} else {
$scope.target.rawQuery = $scope.rawQueryOld;
}
};
$scope.init();
});
});

View File

@@ -0,0 +1,103 @@
define([
'lodash'
],
function (_) {
'use strict';
return {
metricAggTypes: [
{text: "Count", value: 'count' },
{text: "Average", value: 'avg' },
{text: "Sum", value: 'sum' },
{text: "Max", value: 'max' },
{text: "Min", value: 'min' },
{text: "Extended Stats", value: 'extended_stats' },
{text: "Percentiles", value: 'percentiles' },
{text: "Unique Count", value: "cardinality" }
],
bucketAggTypes: [
{text: "Terms", value: 'terms' },
{text: "Date Histogram", value: 'date_histogram' },
],
orderByOptions: [
{text: "Doc Count", value: '_count' },
{text: "Term value", value: '_term' },
],
orderOptions: [
{text: "Top", value: 'desc' },
{text: "Bottom", value: 'asc' },
],
sizeOptions: [
{text: "No limit", value: '0' },
{text: "1", value: '1' },
{text: "2", value: '2' },
{text: "3", value: '4' },
{text: "5", value: '5' },
{text: "10", value: '10' },
{text: "15", value: '15' },
{text: "20", value: '20' },
],
extendedStats: [
{text: 'Avg', value: 'avg'},
{text: 'Min', value: 'min'},
{text: 'Max', value: 'max'},
{text: 'Sum', value: 'sum'},
{text: 'Count', value: 'count'},
{text: 'Std Dev', value: 'std_deviation'},
{text: 'Std Dev Upper', value: 'std_deviation_bounds_upper'},
{text: 'Std Dev Lower', value: 'std_deviation_bounds_lower'},
],
intervalOptions: [
{text: 'auto', value: 'auto'},
{text: '10s', value: '10s'},
{text: '1m', value: '1m'},
{text: '5m', value: '5m'},
{text: '10m', value: '10m'},
{text: '20m', value: '20m'},
{text: '1h', value: '1h'},
{text: '1d', value: '1d'},
],
getOrderByOptions: function(target) {
var self = this;
var metricRefs = [];
_.each(target.metrics, function(metric) {
if (metric.type !== 'count') {
metricRefs.push({text: self.describeMetric(metric), value: metric.id});
}
});
return this.orderByOptions.concat(metricRefs);
},
describeOrder: function(order) {
var def = _.findWhere(this.orderOptions, {value: order});
return def.text;
},
describeMetric: function(metric) {
var def = _.findWhere(this.metricAggTypes, {value: metric.type});
return def.text + ' ' + metric.field;
},
describeOrderBy: function(orderBy, target) {
var def = _.findWhere(this.orderByOptions, {value: orderBy});
if (def) {
return def.text;
}
var metric = _.findWhere(target.metrics, {id: orderBy});
if (metric) {
return this.describeMetric(metric);
} else {
return "metric not found";
}
},
};
});

View File

@@ -49,24 +49,18 @@
</li>
</ul>
<input type="text" class="tight-form-clear-input span10"
ng-model="target.target"
give-focus="target.textEditor"
spellcheck='false'
ng-model-onblur ng-change="get_data()"
ng-show="target.textEditor" />
<input type="text" class="tight-form-clear-input span10" ng-model="target.target" give-focus="target.textEditor" spellcheck='false' ng-model-onblur ng-change="get_data()" ng-show="target.textEditor"></input>
<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
<li ng-repeat="segment in segments" role="menuitem">
<metric-segment segment="segment" get-alt-segments="getAltSegments($index)" on-value-changed="segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
</ul>
<div class="clearfix"></div>
</div>
<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
<li ng-repeat="segment in segments" role="menuitem">
<metric-segment segment="segment" get-options="getAltSegments($index)" on-change="segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -10,7 +10,7 @@ function (angular, _, config, gfunc, Parser) {
var module = angular.module('grafana.controllers');
module.controller('GraphiteQueryCtrl', function($scope, $sce, templateSrv) {
module.controller('GraphiteQueryCtrl', function($scope, uiSegmentSrv, templateSrv) {
$scope.init = function() {
if ($scope.target) {
@@ -104,7 +104,7 @@ function (angular, _, config, gfunc, Parser) {
}
$scope.segments = _.map(astNode.segments, function(segment) {
return new MetricSegment(segment);
return uiSegmentSrv.newSegment(segment);
});
}
}
@@ -119,7 +119,7 @@ function (angular, _, config, gfunc, Parser) {
function checkOtherSegments(fromIndex) {
if (fromIndex === 0) {
$scope.segments.push(MetricSegment.newSelectMetric());
$scope.segments.push(uiSegmentSrv.newSelectMetric());
return;
}
@@ -129,13 +129,13 @@ function (angular, _, config, gfunc, Parser) {
if (segments.length === 0) {
if (path !== '') {
$scope.segments = $scope.segments.splice(0, fromIndex);
$scope.segments.push(MetricSegment.newSelectMetric());
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
return;
}
if (segments[0].expandable) {
if ($scope.segments.length === fromIndex) {
$scope.segments.push(MetricSegment.newSelectMetric());
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
else {
return checkOtherSegments(fromIndex + 1);
@@ -162,14 +162,14 @@ function (angular, _, config, gfunc, Parser) {
return $scope.datasource.metricFindQuery(query).then(function(segments) {
var altSegments = _.map(segments, function(segment) {
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(templateSrv.variables, function(variable) {
altSegments.unshift(new MetricSegment({
altSegments.unshift(uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
@@ -177,7 +177,7 @@ function (angular, _, config, gfunc, Parser) {
});
// add wildcard option
altSegments.unshift(new MetricSegment('*'));
altSegments.unshift(uiSegmentSrv.newSegment('*'));
return altSegments;
})
.then(null, function(err) {
@@ -284,25 +284,6 @@ function (angular, _, config, gfunc, Parser) {
}
};
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
this.expandable = true;
return;
}
this.fake = options.fake;
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
MetricSegment.newSelectMetric = function() {
return new MetricSegment({value: 'select metric', fake: true});
};
$scope.init();
});

View File

@@ -108,25 +108,5 @@ function (_) {
return list;
};
p.createNameForSeries = function(seriesName, groupByColValue) {
var regex = /\$(\w+)/g;
var segments = seriesName.split('.');
return this.alias.replace(regex, function(match, group) {
if (group === 's') {
return seriesName;
}
else if (group === 'g') {
return groupByColValue;
}
var index = parseInt(group);
if (_.isNumber(index) && index < segments.length) {
return segments[index];
}
return match;
});
};
return InfluxSeries;
});

View File

@@ -51,7 +51,7 @@
</span>
</li>
<li>
<metric-segment segment="addFieldSegment" get-alt-segments="getFieldSegments()" on-value-changed="addField()"></metric-segment>
<metric-segment segment="addFieldSegment" get-options="getFieldSegments()" on-change="addField()"></metric-segment>
</li>
</ul>
@@ -64,7 +64,7 @@
FROM
</li>
<li>
<metric-segment segment="measurementSegment" get-alt-segments="getMeasurements()" on-value-changed="measurementChanged()"></metric-segment>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
</li>
</ul>
@@ -78,7 +78,7 @@
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, $index)" on-value-changed="tagSegmentUpdated(segment, $index)"></metric-segment>
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
@@ -95,7 +95,7 @@
</li>
<li ng-repeat="segment in groupBySegments">
<metric-segment segment="segment" get-alt-segments="getGroupByTagSegments(segment, 0)" on-value-changed="groupByTagUpdated(segment, $index)"></metric-segment>
<metric-segment segment="segment" get-options="getGroupByTagSegments(segment, 0)" on-change="groupByTagUpdated(segment, $index)"></metric-segment>
</li>
<li class="dropdown">
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">

View File

@@ -8,7 +8,7 @@ function (angular, _, InfluxQueryBuilder) {
var module = angular.module('grafana.controllers');
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) {
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) {
$scope.init = function() {
if (!$scope.target) { return; }
@@ -24,12 +24,12 @@ function (angular, _, InfluxQueryBuilder) {
$scope.queryBuilder = new InfluxQueryBuilder(target);
if (!target.measurement) {
$scope.measurementSegment = MetricSegment.newSelectMeasurement();
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
$scope.measurementSegment = new MetricSegment(target.measurement);
$scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
}
$scope.addFieldSegment = MetricSegment.newPlusButton();
$scope.addFieldSegment = uiSegmentSrv.newPlusButton();
$scope.tagSegments = [];
_.each(target.tags, function(tag) {
@@ -42,24 +42,25 @@ function (angular, _, InfluxQueryBuilder) {
}
if (tag.condition) {
$scope.tagSegments.push(MetricSegment.newCondition(tag.condition));
$scope.tagSegments.push(uiSegmentSrv.newCondition(tag.condition));
}
$scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key', cssClass: 'query-segment-key' }));
$scope.tagSegments.push(MetricSegment.newOperator(tag.operator));
$scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value', cssClass: 'query-segment-value'}));
$scope.tagSegments.push(uiSegmentSrv.newKey(tag.Key));
$scope.tagSegments.push(uiSegmentSrv.newOperator(tag.operator));
$scope.tagSegments.push(uiSegmentSrv.newKeyValue(tag.value));
});
$scope.fixTagSegments();
$scope.groupBySegments = [];
_.each(target.groupByTags, function(tag) {
$scope.groupBySegments.push(new MetricSegment(tag));
$scope.groupBySegments.push(uiSegmentSrv.newSegment(tag));
});
$scope.groupBySegments.push(MetricSegment.newPlusButton());
$scope.groupBySegments.push(uiSegmentSrv.newPlusButton());
$scope.removeTagFilterSegment = new MetricSegment({fake: true, value: '-- remove tag filter --'});
$scope.removeGroupBySegment = new MetricSegment({fake: true, value: '-- remove group by --'});
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
$scope.removeGroupBySegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove group by --'});
};
$scope.fixTagSegments = function() {
@@ -67,7 +68,7 @@ function (angular, _, InfluxQueryBuilder) {
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
if (!lastSegment || lastSegment.type !== 'plus-button') {
$scope.tagSegments.push(MetricSegment.newPlusButton());
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
};
@@ -80,7 +81,7 @@ function (angular, _, InfluxQueryBuilder) {
}
if (index === $scope.groupBySegments.length-1) {
$scope.groupBySegments.push(MetricSegment.newPlusButton());
$scope.groupBySegments.push(uiSegmentSrv.newPlusButton());
}
segment.type = 'group-by-key';
@@ -131,12 +132,12 @@ function (angular, _, InfluxQueryBuilder) {
$scope.transformToSegments = function(addTemplateVars) {
return function(results) {
var segments = _.map(results, function(segment) {
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
segments.unshift(new MetricSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
});
}
@@ -146,14 +147,14 @@ function (angular, _, InfluxQueryBuilder) {
$scope.getTagsOrValues = function(segment, index) {
if (segment.type === 'condition') {
return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]);
return $q.when([uiSegmentSrv.newSegment('AND'), uiSegmentSrv.newSegment('OR')]);
}
if (segment.type === 'operator') {
var nextValue = $scope.tagSegments[index+1].value;
if (/^\/.*\/$/.test(nextValue)) {
return $q.when(MetricSegment.newOperators(['=~', '!~']));
return $q.when(uiSegmentSrv.newOperators(['=~', '!~']));
} else {
return $q.when(MetricSegment.newOperators(['=', '<>', '<', '>']));
return $q.when(uiSegmentSrv.newOperators(['=', '<>', '<', '>']));
}
}
@@ -186,7 +187,7 @@ function (angular, _, InfluxQueryBuilder) {
$scope.addField = function() {
$scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
_.extend($scope.addFieldSegment, MetricSegment.newPlusButton());
_.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
};
$scope.fieldChanged = function(field) {
@@ -217,27 +218,27 @@ function (angular, _, InfluxQueryBuilder) {
if (segment.value === $scope.removeTagFilterSegment.value) {
$scope.tagSegments.splice(index, 3);
if ($scope.tagSegments.length === 0) {
$scope.tagSegments.push(MetricSegment.newPlusButton());
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
} else if ($scope.tagSegments.length > 2) {
$scope.tagSegments.splice(Math.max(index-1, 0), 1);
if ($scope.tagSegments[$scope.tagSegments.length-1].type !== 'plus-button') {
$scope.tagSegments.push(MetricSegment.newPlusButton());
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
}
}
else {
if (segment.type === 'plus-button') {
if (index > 2) {
$scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND'));
$scope.tagSegments.splice(index, 0, uiSegmentSrv.newCondition('AND'));
}
$scope.tagSegments.push(MetricSegment.newOperator('='));
$scope.tagSegments.push(MetricSegment.newFake('select tag value', 'value', 'query-segment-value'));
$scope.tagSegments.push(uiSegmentSrv.newOperator('='));
$scope.tagSegments.push(uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if ((index+1) === $scope.tagSegments.length) {
$scope.tagSegments.push(MetricSegment.newPlusButton());
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
}
@@ -258,7 +259,7 @@ function (angular, _, InfluxQueryBuilder) {
else if (segment2.type === 'value') {
tagOperator = $scope.getTagValueOperator(segment2.value, tags[tagIndex].operator);
if (tagOperator) {
$scope.tagSegments[index-1] = MetricSegment.newOperator(tagOperator);
$scope.tagSegments[index-1] = uiSegmentSrv.newOperator(tagOperator);
tags[tagIndex].operator = tagOperator;
}
tags[tagIndex].value = segment2.value;
@@ -285,59 +286,6 @@ function (angular, _, InfluxQueryBuilder) {
}
};
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
this.expandable = true;
return;
}
if (_.isString(options)) {
this.value = options;
this.html = $sce.trustAsHtml(this.value);
return;
}
this.cssClass = options.cssClass;
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
MetricSegment.newSelectMeasurement = function() {
return new MetricSegment({value: 'select measurement', fake: true});
};
MetricSegment.newFake = function(text, type, cssClass) {
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
};
MetricSegment.newCondition = function(condition) {
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
};
MetricSegment.newOperator = function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
};
MetricSegment.newOperators = function(ops) {
return _.map(ops, function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
});
};
MetricSegment.newPlusButton = function() {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button' });
};
MetricSegment.newSelectTagValue = function() {
return new MetricSegment({value: 'select tag value', fake: true});
};
$scope.init();
});

View File

@@ -7,6 +7,7 @@ define([
'./keyboardManager',
'./analytics',
'./popoverSrv',
'./uiSegmentSrv',
'./backendSrv',
],
function () {});

View File

@@ -0,0 +1,92 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.service('uiSegmentSrv', function($sce, templateSrv) {
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
this.expandable = true;
return;
}
if (_.isString(options)) {
this.value = options;
this.html = $sce.trustAsHtml(this.value);
return;
}
this.cssClass = options.cssClass;
this.custom = options.custom;
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
this.getSegmentForValue = function(value, fallbackText) {
if (value) {
return this.newSegment(value);
} else {
return this.newSegment({value: fallbackText, fake: true});
}
};
this.newSelectMeasurement = function() {
return new MetricSegment({value: 'select measurement', fake: true});
};
this.newFake = function(text, type, cssClass) {
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
};
this.newSegment = function(options) {
return new MetricSegment(options);
};
this.newKey = function(key) {
return new MetricSegment({value: key, type: 'key', cssClass: 'query-segment-key' });
};
this.newKeyValue = function(value) {
return new MetricSegment({value: value, type: 'value', cssClass: 'query-segment-value' });
};
this.newCondition = function(condition) {
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
};
this.newOperator = function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
};
this.newOperators = function(ops) {
return _.map(ops, function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
});
};
this.newSelectMetric = function() {
return new MetricSegment({value: 'select metric', fake: true});
};
this.newPlusButton = function() {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button' });
};
this.newSelectTagValue = function() {
return new MetricSegment({value: 'select tag value', fake: true});
};
});
});