mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'elastic_ds'
Conflicts: public/app/plugins/datasource/influxdb/queryCtrl.js
This commit is contained in:
@@ -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: {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
Basic Auth
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Enable
|
||||
<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
|
||||
|
||||
16
public/app/plugins/datasource/cloudwatch/_plugin.json
Normal file
16
public/app/plugins/datasource/cloudwatch/_plugin.json
Normal 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
|
||||
}
|
||||
561
public/app/plugins/datasource/cloudwatch/datasource.js
Normal file
561
public/app/plugins/datasource/cloudwatch/datasource.js
Normal 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;
|
||||
});
|
||||
|
||||
});
|
||||
13
public/app/plugins/datasource/cloudwatch/directives.js
Normal file
13
public/app/plugins/datasource/cloudwatch/directives.js
Normal 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'};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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}} = {{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>
|
||||
|
||||
172
public/app/plugins/datasource/cloudwatch/queryCtrl.js
Normal file
172
public/app/plugins/datasource/cloudwatch/queryCtrl.js
Normal 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();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
115
public/app/plugins/datasource/elasticsearch/bucketAgg.js
Normal file
115
public/app/plugins/datasource/elasticsearch/bucketAgg.js
Normal 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();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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: "&",
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
187
public/app/plugins/datasource/elasticsearch/elasticResponse.js
Normal file
187
public/app/plugins/datasource/elasticsearch/elasticResponse.js
Normal 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;
|
||||
});
|
||||
48
public/app/plugins/datasource/elasticsearch/indexPattern.js
Normal file
48
public/app/plugins/datasource/elasticsearch/indexPattern.js
Normal 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;
|
||||
});
|
||||
93
public/app/plugins/datasource/elasticsearch/metricAgg.js
Normal file
93
public/app/plugins/datasource/elasticsearch/metricAgg.js
Normal 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();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"annotations": "app/plugins/datasource/elasticsearch/partials/annotations.editor.html"
|
||||
},
|
||||
|
||||
"annotations": true
|
||||
"annotations": true,
|
||||
"metrics": true
|
||||
}
|
||||
|
||||
134
public/app/plugins/datasource/elasticsearch/queryBuilder.js
Normal file
134
public/app/plugins/datasource/elasticsearch/queryBuilder.js
Normal 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;
|
||||
|
||||
});
|
||||
69
public/app/plugins/datasource/elasticsearch/queryCtrl.js
Normal file
69
public/app/plugins/datasource/elasticsearch/queryCtrl.js
Normal 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();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
103
public/app/plugins/datasource/elasticsearch/queryDef.js
Normal file
103
public/app/plugins/datasource/elasticsearch/queryDef.js
Normal 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";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ define([
|
||||
'./keyboardManager',
|
||||
'./analytics',
|
||||
'./popoverSrv',
|
||||
'./uiSegmentSrv',
|
||||
'./backendSrv',
|
||||
],
|
||||
function () {});
|
||||
|
||||
92
public/app/services/uiSegmentSrv.js
Normal file
92
public/app/services/uiSegmentSrv.js
Normal 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});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user