Compound alarm expression
Commits provides ability to specify alarm expressions composed of multiple sub-expressions. Following features/changes are made: * responsive UI based on browser window size * rewrite expression widget to act as full-featured Angular directive * directive submits compiled expression into hidden input * any error in any of sub expressions form results in clearing the expression value and effectively prevents user from reaching next step * sub-expressions can be: ** added ** removed ** reorganized Change-Id: I1b4bd51e8887bc44dd3e2ffab4900d29fc77b77e
This commit is contained in:
parent
a181c1cc34
commit
58367e3e00
@ -60,7 +60,6 @@ class ExpressionWidget(forms.Widget):
|
||||
t = get_template(constants.TEMPLATE_PREFIX + 'expression_field.html')
|
||||
|
||||
local_attrs = {
|
||||
'service': '',
|
||||
'func': ExpressionWidget.func,
|
||||
'comparators': ExpressionWidget.comparators,
|
||||
'operators': ExpressionWidget.operators,
|
||||
|
@ -17,6 +17,7 @@
|
||||
import logging
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import tables
|
||||
@ -30,12 +31,11 @@ LOG = logging.getLogger(__name__)
|
||||
class CreateAlarm(tables.LinkAction):
|
||||
name = "create_alarm"
|
||||
verbose_name = _("Create Alarm Definition")
|
||||
classes = ("ajax-modal",)
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
icon = "plus"
|
||||
policy_rules = (("alarm", "alarm:create"),)
|
||||
ajax = True
|
||||
|
||||
|
||||
def get_link_url(self):
|
||||
return urlresolvers.reverse(constants.URL_PREFIX + 'alarm_create',
|
||||
args=())
|
||||
|
@ -8,7 +8,6 @@
|
||||
{% block css %}
|
||||
{% include "_stylesheets.html" %}
|
||||
<link href='{{ STATIC_URL }}monitoring/css/ng-tags-input.css' type="text/css" rel="stylesheet"/>
|
||||
<link href='{{ STATIC_URL }}monitoring/css/alarm-create.css' type="text/css" rel="stylesheet"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
|
@ -1,95 +1,4 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block js %}{% spaceless %}
|
||||
<script type="text/javascript">
|
||||
window._alarm_edit_ctrl_metrics = {{ metrics|safe|default:"[]" }}
|
||||
</script>
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
|
||||
<div class="alarm-expression">
|
||||
|
||||
<input type="hidden" name="{{ name }}" id="expression">
|
||||
|
||||
<div class="row expression-details">
|
||||
<div class="col-md-2">
|
||||
<select id="function"
|
||||
class="form-control"
|
||||
aria-label="{% trans 'Function' %}"
|
||||
title="{% trans 'Function' %}"
|
||||
ng-model="currentFunction"
|
||||
ng-options="f[0] as f[1] for f in {{func}}"
|
||||
ng-change="saveExpression()"></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<select id="metric-chooser"
|
||||
class="form-control"
|
||||
aria-label="{% trans 'Metric' %}"
|
||||
title="{% trans 'Metric' %}"
|
||||
ng-model="currentMetric"
|
||||
ng-options="metric for metric in metricNames"
|
||||
ng-change="metricChanged()"></select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-control"
|
||||
aria-label="{% trans 'Comparator' %}"
|
||||
title="{% trans 'Comparator' %}"
|
||||
ng-model="currentComparator"
|
||||
ng-options="f[0] as f[1] for f in {{comparators}}"
|
||||
ng-change="saveExpression()"></select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number"
|
||||
step="any"
|
||||
class="form-control"
|
||||
aria-label="{% trans 'Threshold' %}"
|
||||
title="{% trans 'Threshold' %}"
|
||||
ng-model="currentThreshold"
|
||||
ng-change="saveExpression()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expression-details">
|
||||
<div class="col-md-10">
|
||||
<tags-input id="dimension-chooser"
|
||||
ng-model="tags"
|
||||
placeholder="{% trans 'Add a dimension' %}"
|
||||
add-from-autocomplete-only="true"
|
||||
on-tag-added="saveDimension()"
|
||||
on-tag-removed="saveDimension()">
|
||||
<auto-complete source="possibleDimensions($query)"
|
||||
max-results-to-show="30"
|
||||
min-length="1">
|
||||
</auto-complete>
|
||||
</tags-input>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label id="is-deterministic-expression"
|
||||
class="btn expression-deterministic"
|
||||
ng-class="{'btn-primary': currentIsDeterministic, 'btn-default': !currentIsDeterministic}"
|
||||
ng-click="currentIsDeterministic = !currentIsDeterministic;saveExpression()"
|
||||
ng-model="currentIsDeterministic">{% trans 'Deterministic' %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expression-details">
|
||||
<div class="topologyBalloon" id="metrics" style="position:static;display: block;">
|
||||
<div class="contentBody">
|
||||
<table class="detailInfoTable">
|
||||
<caption>Matching Metrics</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th ng-repeat="name in dimnames">{$name$}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="metric in matchingMetrics">
|
||||
<td ng-repeat="dim in dimnames" style="white-space:normal">{$metric[dim] | spacedim $}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<mon-alarm-expression metrics="{{ metrics|default:'[]' }}"
|
||||
functions="{{ func }}"
|
||||
comparators="{{ comparators }}"
|
||||
operators="{{ operators }}"></mon-alarm-expression>
|
||||
|
@ -1,16 +0,0 @@
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
{{ step.get_help_text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-12"
|
||||
ng-controller="alarmEditController"
|
||||
ng-init="init('{{ service }}')">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,12 +1,15 @@
|
||||
{% load i18n %}
|
||||
<div>
|
||||
<div ng-controller="alarmMatchByController as ctrl">
|
||||
<input type="hidden" name="{{ name }}" id="id_{{ name }}"/>
|
||||
<tags-input id="dimkey-chooser" ng-model="matchByTags"
|
||||
<tags-input id="dimkey-chooser"
|
||||
ng-model="ctrl.matchByTags"
|
||||
placeholder="{% trans 'Add a match by' %}"
|
||||
add-from-autocomplete-only="true"
|
||||
on-tag-added="saveDimKey()" on-tag-removed="saveDimKey()">
|
||||
<auto-complete source="possibleDimKeys($query)"
|
||||
max-results-to-show="30" min-length="1">
|
||||
on-tag-added="ctrl.saveDimKey()"
|
||||
on-tag-removed="ctrl.saveDimKey()">
|
||||
<auto-complete source="ctrl.possibleDimKeys($query)"
|
||||
max-results-to-show="30"
|
||||
min-length="1">
|
||||
</auto-complete>
|
||||
</tags-input>
|
||||
</div>
|
||||
|
@ -2,13 +2,14 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
<!-- hide if window gets very small to save some space -->
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 hidden-xs">
|
||||
{{ step.get_help_text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -82,7 +82,7 @@ class AlarmDefinitionsTest(helpers.TestCase):
|
||||
self.assertContains(res, '<select class="form-control" '
|
||||
'id="id_severity"')
|
||||
|
||||
self.assertContains(res, '<input type="hidden" name="expression"')
|
||||
self.assertContains(res, '<mon-alarm-expression')
|
||||
|
||||
self.assertContains(res, '<input type="hidden" name="alarm_actions"')
|
||||
self.assertContains(res, '<input type="hidden" name="ok_actions"')
|
||||
|
@ -156,12 +156,13 @@ class SetAlarmDefinitionExpressionAction(workflows.Action):
|
||||
class SetDetailsStep(workflows.Step):
|
||||
action_class = SetAlarmDefinitionAction
|
||||
contributes = ('name', 'description', 'severity')
|
||||
template_name = 'monitoring/alarmdefs/workflow_step.html'
|
||||
|
||||
|
||||
class SetExpressionStep(workflows.Step):
|
||||
action_class = SetAlarmDefinitionExpressionAction
|
||||
contributes = ('expression', 'match_by')
|
||||
template_name = 'monitoring/alarmdefs/expression_step.html'
|
||||
template_name = 'monitoring/alarmdefs/workflow_step.html'
|
||||
|
||||
def contribute(self, data, context):
|
||||
context = (super(SetExpressionStep, self)
|
||||
@ -181,7 +182,7 @@ class SetExpressionStep(workflows.Step):
|
||||
class SetNotificationsStep(workflows.Step):
|
||||
action_class = SetAlarmNotificationsAction
|
||||
contributes = ('alarm_actions', 'ok_actions', 'undetermined_actions')
|
||||
template_name = 'monitoring/alarmdefs/notification_step.html'
|
||||
template_name = 'monitoring/alarmdefs/workflow_step.html'
|
||||
|
||||
|
||||
class AlarmDefinitionWorkflow(workflows.Workflow):
|
||||
|
@ -8,9 +8,15 @@ ADD_ANGULAR_MODULES = ['monitoringApp']
|
||||
|
||||
# A list of javascript files to be included for all pages
|
||||
ADD_JS_FILES = ['monitoring/js/app.js',
|
||||
'monitoring/js/filters.js',
|
||||
'monitoring/js/controllers.js',
|
||||
'monitoring/js/directives.js',
|
||||
'monitoring/js/services.js',
|
||||
'monitoring/js/ng-tags-input.js']
|
||||
|
||||
ADD_SCSS_FILES = [
|
||||
'monitoring/css/alarm-create.scss']
|
||||
|
||||
from monascaclient import exc
|
||||
# A dictionary of exception classes to be added to HORIZON['exceptions'].
|
||||
ADD_EXCEPTIONS = {
|
||||
|
@ -1,12 +0,0 @@
|
||||
.alarm-expression .row {
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
.alarm-expression .expression-details {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.alarm-expression .expression-details > div {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
60
monitoring/static/monitoring/css/alarm-create.scss
Normal file
60
monitoring/static/monitoring/css/alarm-create.scss
Normal file
@ -0,0 +1,60 @@
|
||||
.alarm-expression {
|
||||
|
||||
.row {
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
pre.expression-preview {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
}
|
||||
|
||||
pre.expression-valid {
|
||||
border-color: #00B700;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
pre.expression-invalid {
|
||||
border-color: #A94442;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.alarm-sub-expression {
|
||||
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
.sub-expression-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.expression-details {
|
||||
margin-bottom: 5px;
|
||||
|
||||
div {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
|
||||
.has-error {
|
||||
border-color: #a94442;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Declare app level module which depends on filters, and services
|
||||
angular.module('monitoringApp', [
|
||||
'monitoring.controllers', 'ngTagsInput', 'monitoring.filters'
|
||||
]);
|
||||
angular
|
||||
.module('monitoringApp', [
|
||||
'monitoring.controllers',
|
||||
'monitoring.directives',
|
||||
'monitoring.filters',
|
||||
'monitoring.services',
|
||||
'ngTagsInput'
|
||||
])
|
||||
.config(config);
|
||||
|
||||
config.$inject = ['$provide', '$windowProvider'];
|
||||
|
||||
function config($provide, $windowProvider) {
|
||||
var path = $windowProvider.$get().STATIC_URL + 'monitoring/widgets/';
|
||||
$provide.constant('monitoringApp.staticPath', path);
|
||||
}
|
||||
|
@ -127,173 +127,63 @@ angular.module('monitoring.controllers', [])
|
||||
};
|
||||
|
||||
}])
|
||||
.controller('alarmEditController', [
|
||||
"$window", "$scope", "$http", "$timeout", "$q",
|
||||
function ($window, $scope, $http, $timeout, $q) {
|
||||
.controller('alarmNotificationFieldController',
|
||||
['$rootScope', NotificationField]
|
||||
)
|
||||
.controller('alarmMatchByController',
|
||||
['$q', '$rootScope', MatchByController]
|
||||
);
|
||||
|
||||
$scope.metrics = [];
|
||||
$scope.metricNames = []
|
||||
$scope.currentMetric = undefined;
|
||||
function MatchByController($q, $rootScope) {
|
||||
// model
|
||||
var vm = this;
|
||||
|
||||
$scope.currentFunction = "max";
|
||||
$scope.currentComparator = ">";
|
||||
$scope.currentThreshold = 0;
|
||||
$scope.currentIsDeterministic = false;
|
||||
$scope.matchingMetrics = [];
|
||||
$scope.tags = [];
|
||||
$scope.matchByTags = [];
|
||||
vm.matchBy = [];
|
||||
vm.matchByTags = [];
|
||||
|
||||
$scope.possibleDimensions = function(query) {
|
||||
return $q(function(resolve, reject) {
|
||||
var dim = {}
|
||||
var dimList = []
|
||||
angular.forEach($scope.matchingMetrics, function(value, name) {
|
||||
for (var key in value.dimensions) {
|
||||
if (value.dimensions.hasOwnProperty(key)) {
|
||||
var dimStr = key + "=" + value.dimensions[key]
|
||||
if (dimStr.indexOf(query) === 0) {
|
||||
dim[dimStr] = dimStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
angular.forEach(dim, function(value, name) {
|
||||
// api
|
||||
vm.saveDimKey = saveDimKey;
|
||||
vm.possibleDimKeys = possibleDimKeys;
|
||||
|
||||
function possibleDimKeys(query) {
|
||||
return $q(function(resolve, reject) {
|
||||
var dimList = [];
|
||||
angular.forEach(vm.matchBy, function(value) {
|
||||
if (value.indexOf(query) === 0) {
|
||||
dimList.push(value);
|
||||
});
|
||||
resolve(dimList);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.possibleDimKeys = function(query) {
|
||||
return $q(function(resolve, reject) {
|
||||
var dimList = []
|
||||
angular.forEach($scope.matchingMetrics, function(value, name) {
|
||||
for (var key in value.dimensions) {
|
||||
if (key.indexOf(query) === 0) {
|
||||
if (dimList.indexOf(key) < 0) {
|
||||
dimList.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(dimList);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.metricChanged = function() {
|
||||
if ($scope.defaultTag.length > 0) {
|
||||
$scope.tags = [{text: $scope.defaultTag}];
|
||||
}
|
||||
$scope.saveDimension();
|
||||
}
|
||||
|
||||
$scope.saveExpression = function() {
|
||||
$('#expression').val($scope.formatDimension());
|
||||
}
|
||||
|
||||
$scope.saveDimension = function() {
|
||||
$scope.saveExpression();
|
||||
|
||||
var mm = []
|
||||
angular.forEach($scope.metrics, function(value, key) {
|
||||
if (value.name === $scope.currentMetric) {
|
||||
var match = true;
|
||||
for (var i = 0; i < $scope.tags.length; i++) {
|
||||
var vals = $scope.tags[i]['text'].split('=');
|
||||
if (value.dimensions[vals[0]] !== vals[1]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
mm.push(value)
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.matchingMetrics = mm
|
||||
$scope.dimnames = ['name', 'dimensions'];
|
||||
$('#match').val($scope.formatMatchBy());
|
||||
resolve(dimList);
|
||||
});
|
||||
}
|
||||
|
||||
function saveDimKey() {
|
||||
var matchByTags = []
|
||||
for (var i = 0; i < vm.matchByTags.length; i++) {
|
||||
matchByTags.push(vm.matchByTags[i]['text'])
|
||||
}
|
||||
$('#id_match_by').val(matchByTags.join(','));
|
||||
}
|
||||
|
||||
// init
|
||||
$rootScope.$on('$destroy', (function() {
|
||||
|
||||
var watcher = $rootScope.$on('mon_match_by_changed', onMatchByChange);
|
||||
|
||||
return function destroyer() {
|
||||
watcher();
|
||||
}
|
||||
|
||||
$scope.saveDimKey = function() {
|
||||
var matchByTags = []
|
||||
for (var i = 0; i < $scope.matchByTags.length; i++) {
|
||||
matchByTags.push($scope.matchByTags[i]['text'])
|
||||
}
|
||||
$('#id_match_by').val(matchByTags.join(','));
|
||||
}
|
||||
|
||||
$scope.formatDimension = function() {
|
||||
var dim = '';
|
||||
angular.forEach($scope.tags, function(value, key) {
|
||||
if (dim.length) {
|
||||
dim += ',';
|
||||
}
|
||||
dim += value['text'];
|
||||
})
|
||||
return $scope.currentFunction
|
||||
+ '('
|
||||
+ $scope.currentMetric
|
||||
+ '{' + dim + '}'
|
||||
+ ($scope.currentIsDeterministic ? ',deterministic' : '')
|
||||
+ ') '
|
||||
+ $scope.currentComparator
|
||||
+ ' '
|
||||
+ $scope.currentThreshold;
|
||||
}
|
||||
|
||||
$scope.formatMatchBy = function() {
|
||||
var dimNames = {}
|
||||
for (var i = 0; i < $scope.matchingMetrics.length; i++) {
|
||||
for (var attrname in $scope.matchingMetrics[i].dimensions) { dimNames[attrname] = true; }
|
||||
}
|
||||
var matches = [];
|
||||
for (var attrname in dimNames) { matches.push(attrname); }
|
||||
return matches;
|
||||
}
|
||||
|
||||
$scope.init = function(defaultTag) {
|
||||
|
||||
if (defaultTag.length > 0) {
|
||||
$scope.tags = [{text: defaultTag}];
|
||||
}
|
||||
|
||||
$scope.defaultTag = defaultTag;
|
||||
|
||||
metrics = $window._alarm_edit_ctrl_metrics
|
||||
|
||||
$scope.metrics = metrics && metrics.length ? metrics : [];
|
||||
$scope.metricNames = uniqueNames($scope.metrics, 'name');
|
||||
$scope.currentMetric = $scope.metricNames[0];
|
||||
|
||||
$scope.saveDimension();
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', (function() {
|
||||
var detWatcher = $scope.$watch('currentIsDeterministic', function detWatcher(newValue, oldValue) {
|
||||
if(newValue != oldValue){
|
||||
$scope.$emit('mon_deterministic_changed', newValue);
|
||||
}
|
||||
function onMatchByChange(event, matchBy) {
|
||||
// remove from tags those match by that do not match
|
||||
vm.matchByTags = vm.matchByTags.filter(function filter(tag){
|
||||
return matchBy.indexOf(tag['text']) >= 0;
|
||||
});
|
||||
return function() {
|
||||
// destroy watchers
|
||||
detWatcher();
|
||||
}
|
||||
}()));
|
||||
|
||||
function uniqueNames(input, key) {
|
||||
var unique = {};
|
||||
var uniqueList = [];
|
||||
for(var i = 0; i < input.length; i++){
|
||||
if(typeof unique[input[i][key]] == "undefined"){
|
||||
unique[input[i][key]] = "";
|
||||
uniqueList.push(input[i][key]);
|
||||
}
|
||||
}
|
||||
return uniqueList.sort();
|
||||
vm.matchBy = matchBy || [];
|
||||
}
|
||||
}])
|
||||
.controller('alarmNotificationFieldController', NotificationField);
|
||||
|
||||
}()));
|
||||
}
|
||||
|
||||
function NotificationField($rootScope) {
|
||||
|
||||
@ -316,11 +206,18 @@ function NotificationField($rootScope) {
|
||||
data.forEach(prepareNotify);
|
||||
};
|
||||
vm.add = function(){
|
||||
if(vm.select.model){
|
||||
vm.list.push(allOptions[vm.select.model]);
|
||||
var opt;
|
||||
if (vm.select.model) {
|
||||
opt = allOptions[vm.select.model];
|
||||
|
||||
oldUndetermined[opt.id] = opt.undetermined;
|
||||
opt.undetermined = !vm.isDeterministic;
|
||||
|
||||
vm.list.push(opt);
|
||||
|
||||
removeFromSelect();
|
||||
vm.select.model = null;
|
||||
vm.select.model = undefined;
|
||||
|
||||
}
|
||||
};
|
||||
vm.remove = function(id){
|
||||
@ -337,7 +234,7 @@ function NotificationField($rootScope) {
|
||||
}
|
||||
};
|
||||
|
||||
$rootScope.$on('mon_deterministic_changed', onDeterministicChange)
|
||||
$rootScope.$on('mon_deterministic_changed', onDeterministicChange);
|
||||
|
||||
function prepareNotify(item){
|
||||
var selected = item[7]
|
||||
@ -371,9 +268,7 @@ function NotificationField($rootScope) {
|
||||
|
||||
function onDeterministicChange(event, isDeterministic) {
|
||||
|
||||
if (!(vm.list && vm.list.length)) {
|
||||
return;
|
||||
} else if (isDeterministic === vm.isDeterministic) {
|
||||
if (isDeterministic === vm.isDeterministic) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -393,14 +288,3 @@ function NotificationField($rootScope) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NotificationField.$inject = ['$rootScope'];
|
||||
|
||||
angular.module('monitoring.filters', [])
|
||||
.filter('spacedim', function () {
|
||||
return function(text) {
|
||||
if (typeof text == "string")
|
||||
return text;
|
||||
return JSON.stringify(text).split(',').join(', ');
|
||||
}
|
||||
});
|
||||
|
366
monitoring/static/monitoring/js/directives.js
Normal file
366
monitoring/static/monitoring/js/directives.js
Normal file
@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright 2016 FUJITSU LIMITED
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('monitoring.directives', [
|
||||
'horizon.framework.widgets',
|
||||
'monitoring.filters',
|
||||
'monitoring.services',
|
||||
'gettext'
|
||||
])
|
||||
.directive('monAlarmExpression',
|
||||
['monitoringApp.staticPath', monAlarmExpressionsDirective]
|
||||
)
|
||||
.directive('monAlarmSubExpression',
|
||||
['monitoringApp.staticPath', monAlarmSubExpressionDirective]
|
||||
);
|
||||
|
||||
function monAlarmExpressionsDirective(staticPath){
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
metrics: '=metrics',
|
||||
functions: '=functions',
|
||||
operators: '=operators',
|
||||
comparators: '=comparators',
|
||||
connectable: '=connectable'
|
||||
},
|
||||
templateUrl: staticPath + 'expression/expression.tpl.html',
|
||||
controller: ['$q', '$scope', 'monExpressionBuilder', AlarmExpressionController],
|
||||
controllerAs: 'vm',
|
||||
bindToController: true
|
||||
};
|
||||
|
||||
function AlarmExpressionController($q, $scope, monExpressionBuilder) {
|
||||
// private
|
||||
var vm = this,
|
||||
deterministic = false,
|
||||
matchBy = undefined;
|
||||
|
||||
// scope
|
||||
vm.expression = '';
|
||||
vm.subExpressions = undefined;
|
||||
vm.expressionValid = true;
|
||||
|
||||
// api
|
||||
vm.touch = touch;
|
||||
vm.addExpression = addExpression;
|
||||
vm.removeExpression = removeExpression;
|
||||
vm.reorderExpression = reorderExpression;
|
||||
|
||||
// listen
|
||||
$scope.$on('$destroy', destroy);
|
||||
|
||||
// init
|
||||
$scope.$applyAsync(init);
|
||||
|
||||
function addExpression($event, $index) {
|
||||
if ($event) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
|
||||
vm.subExpressions.splice($index, 0, {});
|
||||
if ($index >= 1 && vm.subExpressions[$index - 1].$valid) {
|
||||
// hide previous expression
|
||||
// if it is valid
|
||||
vm.subExpressions[$index -1]['preview'] = true;
|
||||
}
|
||||
|
||||
applyConnectable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeExpression($event, index) {
|
||||
if ($event) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
vm.subExpressions.splice(index, 1);
|
||||
|
||||
applyConnectable();
|
||||
touch();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function reorderExpression($event, which, where) {
|
||||
$event.preventDefault();
|
||||
vm.subExpressions[where]['op'] = [
|
||||
vm.subExpressions[which]['op'],
|
||||
vm.subExpressions[which]['op'] = vm.subExpressions[where]['op']
|
||||
][0];
|
||||
vm.subExpressions[where] = vm.subExpressions.splice(which, 1, vm.subExpressions[where])[0];
|
||||
|
||||
applyConnectable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function touch() {
|
||||
var hasInvalid = false;
|
||||
expression = undefined;
|
||||
|
||||
matchBy = [];
|
||||
deterministic = true;
|
||||
|
||||
angular.forEach(vm.subExpressions, subExpressionIt);
|
||||
|
||||
if (hasInvalid) {
|
||||
expression = undefined;
|
||||
} else {
|
||||
$scope.$emit('mon_match_by_changed', matchBy.sort());
|
||||
$scope.$emit('mon_deterministic_changed', deterministic);
|
||||
|
||||
expression = monExpressionBuilder.asString(vm.subExpressions, true);
|
||||
|
||||
// change preview only if valid
|
||||
vm.expression = expression;
|
||||
}
|
||||
|
||||
// update that always, regardless if it's valid or not
|
||||
// for invalid case that will reset input's value to empty
|
||||
// disallowing form to be accepted by django
|
||||
$('#expression').val(expression);
|
||||
vm.expressionValid = !hasInvalid;
|
||||
|
||||
return true;
|
||||
|
||||
function subExpressionIt(expr){
|
||||
if (!expr.$valid) {
|
||||
return !(hasInvalid = true);
|
||||
}
|
||||
|
||||
angular.forEach(expr.matchBy || [], function it(mb){
|
||||
if(matchBy.indexOf(mb) < 0){
|
||||
matchBy.push(mb);
|
||||
}
|
||||
});
|
||||
deterministic = deterministic && (expr.deterministic || false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
if(vm.metrics.length) {
|
||||
vm.subExpressions = [];
|
||||
vm.matchBy = [];
|
||||
|
||||
addExpression(undefined, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
delete vm.metrics;
|
||||
delete vm.expression;
|
||||
delete vm.subExpressions;
|
||||
delete vm.deterministic;
|
||||
}
|
||||
|
||||
function applyConnectable() {
|
||||
var count = vm.subExpressions.length;
|
||||
|
||||
switch(count) {
|
||||
case 1:
|
||||
vm.subExpressions[0]['connectable'] = false;
|
||||
break;
|
||||
default: {
|
||||
angular.forEach(vm.subExpressions, function(expr, index) {
|
||||
expr.connectable = index >= 1 && index < vm.subExpressions.length;
|
||||
if (!expr.connectable) {
|
||||
delete expr['op'];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function monAlarmSubExpressionDirective(staticPath) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^monAlarmExpression',
|
||||
scope: {
|
||||
metrics: '=metrics',
|
||||
functions: '=functions',
|
||||
comparators: '=comparators',
|
||||
operators: '=operators',
|
||||
model: '=subExpression',
|
||||
connectable: '=connectable',
|
||||
preview: '=preview'
|
||||
},
|
||||
templateUrl: staticPath + 'expression/sub-expression.tpl.html',
|
||||
link: linkFn,
|
||||
controller: ['$q', '$scope', 'monExpressionBuilder', AlarmSubExpressionController],
|
||||
controllerAs: 'vm',
|
||||
bindToController: true
|
||||
};
|
||||
|
||||
function linkFn(scope, el, attrs, monAlarmExpressions) {
|
||||
el.on('$destroy', (function(){
|
||||
|
||||
var watcher = scope.$watch('vm.model', function(expr, oldExpr) {
|
||||
if (expr !== oldExpr) {
|
||||
monAlarmExpressions.touch();
|
||||
}
|
||||
}, true);
|
||||
|
||||
return function destroyer() {
|
||||
watcher();
|
||||
};
|
||||
|
||||
}()));
|
||||
}
|
||||
|
||||
function AlarmSubExpressionController($q, $scope, monExpressionBuilder) {
|
||||
var vm = this;
|
||||
|
||||
vm.tags = [];
|
||||
vm.matchingMetrics = [];
|
||||
|
||||
// api
|
||||
vm.possibleDimensions = possibleDimensions;
|
||||
vm.onMetricChanged = onMetricChanged;
|
||||
vm.onDimensionsUpdated = onDimensionsUpdated;
|
||||
|
||||
vm.updateExpression = updateExpression;
|
||||
vm.resetExpression = resetExpression;
|
||||
vm.updateExpression = updateExpression;
|
||||
|
||||
// init
|
||||
$scope.$on('$destroy', destroyerFactory());
|
||||
|
||||
function opRemoverListener(nval) {
|
||||
if (vm.model && 'op' in vm.model && !nval) {
|
||||
delete vm.model['op'];
|
||||
}
|
||||
}
|
||||
|
||||
function destroyerFactory() {
|
||||
|
||||
var watcher = $scope.$watch('vm.model.connectable', opRemoverListener, true);
|
||||
|
||||
return function destroyer() {
|
||||
watcher();
|
||||
|
||||
delete vm.tags;
|
||||
delete vm.matchingMetrics;
|
||||
delete vm.model;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateExpression() {
|
||||
var dim = [],
|
||||
formController = $scope.$$childHead.subExpressionForm;
|
||||
|
||||
vm.model.$valid = !formController.$invalid;
|
||||
|
||||
if (vm.model.$valid) {
|
||||
|
||||
if (vm.tags.length > 0) {
|
||||
angular.forEach(vm.tags, function(value, key) {
|
||||
dim.push(value['text']);
|
||||
});
|
||||
vm.model.dimensions = dim;
|
||||
} else {
|
||||
vm.model.dimensions = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resetExpression() {
|
||||
vm.matchingMetrics = [];
|
||||
vm.tags = [];
|
||||
}
|
||||
|
||||
function possibleDimensions(query) {
|
||||
return $q(function(resolve) {
|
||||
var dim = {},
|
||||
dimList = [];
|
||||
|
||||
angular.forEach(vm.matchingMetrics, function(value, name) {
|
||||
for (var key in value.dimensions) {
|
||||
if (value.dimensions.hasOwnProperty(key)) {
|
||||
var dimStr = key + "=" + value.dimensions[key];
|
||||
if (dimStr.indexOf(query) === 0) {
|
||||
dim[dimStr] = dimStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
angular.forEach(dim, function(value, name) {
|
||||
dimList.push(value);
|
||||
});
|
||||
|
||||
resolve(dimList);
|
||||
});
|
||||
}
|
||||
|
||||
function onDimensionsUpdated() {
|
||||
onMetricChanged(vm.model.metric);
|
||||
}
|
||||
|
||||
function onMetricChanged(metric) {
|
||||
handleMetricChanged(metric);
|
||||
updateExpression();
|
||||
}
|
||||
|
||||
function handleMetricChanged(metric) {
|
||||
var mm = [],
|
||||
matchBy = [],
|
||||
tags = vm.tags || [];
|
||||
|
||||
angular.forEach(vm.metrics, function(value, key) {
|
||||
if (value.name === metric.name) {
|
||||
var match = true;
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var vals = tags[i]['text'].split('=');
|
||||
if (value.dimensions[vals[0]] !== vals[1]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if (match) {
|
||||
mm.push(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
angular.forEach(mm, function(value, key){
|
||||
angular.forEach(value.dimensions, function(value, key){
|
||||
if(matchBy.indexOf(key) < 0){
|
||||
matchBy.push(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
vm.matchingMetrics = mm;
|
||||
vm.model.matchBy = matchBy.sort();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
49
monitoring/static/monitoring/js/filters.js
Normal file
49
monitoring/static/monitoring/js/filters.js
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2016 FUJITSU LIMITED
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('monitoring.filters', [
|
||||
'monitoring.services'
|
||||
])
|
||||
.filter('monUniqueMetric', uniqueMetricFilterFactory)
|
||||
.filter('monExpression', ['monExpressionBuilder', monExpressionFilterFactory]);
|
||||
|
||||
function monExpressionFilterFactory(monExpressionBuilder) {
|
||||
|
||||
return function monExpressionFilter(value, withOp) {
|
||||
return monExpressionBuilder.asString([value], withOp);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function uniqueMetricFilterFactory() {
|
||||
|
||||
return function uniqueMetricFilter(arr) {
|
||||
return uniqueNames(arr, 'name');
|
||||
};
|
||||
|
||||
function uniqueNames(input, key) {
|
||||
var unique = {};
|
||||
var uniqueList = [];
|
||||
for(var i = 0; i < input.length; i++){
|
||||
if(typeof unique[input[i][key]] === 'undefined'){
|
||||
unique[input[i][key]] = '';
|
||||
uniqueList.push(input[i]);
|
||||
}
|
||||
}
|
||||
return uniqueList;
|
||||
}
|
||||
}
|
72
monitoring/static/monitoring/js/services.js
Normal file
72
monitoring/static/monitoring/js/services.js
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2016 FUJITSU LIMITED
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('monitoring.services', [])
|
||||
.factory('monExpressionBuilder', expressionBuilder);
|
||||
|
||||
function expressionBuilder() {
|
||||
|
||||
return {
|
||||
asString: subExpressionToString
|
||||
};
|
||||
|
||||
|
||||
function subExpressionToString(subExpressions, withOp) {
|
||||
var tmp = [],
|
||||
exprAsStr;
|
||||
|
||||
angular.forEach(subExpressions, function(expr) {
|
||||
exprAsStr = [
|
||||
withOp ? renderOp(expr) : '',
|
||||
expr.fun || '',
|
||||
expr.fun && '(',
|
||||
expr.metric ? expr.metric.name : '', renderDimensions(expr),
|
||||
(expr.deterministic ? ',deterministic': ''),
|
||||
expr.fun && ')',
|
||||
expr.cmp || '',
|
||||
expr.threshold || ''
|
||||
].join('');
|
||||
tmp.push(exprAsStr);
|
||||
});
|
||||
|
||||
return tmp.join('');
|
||||
}
|
||||
|
||||
function renderDimensions(expr) {
|
||||
var tmp = [];
|
||||
|
||||
if (angular.isUndefined(expr.dimensions) || !expr.dimensions.length){
|
||||
return tmp.join('');
|
||||
}
|
||||
|
||||
tmp.push('{');
|
||||
tmp.push(expr.dimensions.join(','));
|
||||
tmp.push('}');
|
||||
|
||||
return tmp.join('');
|
||||
}
|
||||
|
||||
function renderOp(expr) {
|
||||
var tmp = [];
|
||||
if ('op' in expr) {
|
||||
tmp.push(' ');
|
||||
tmp.push(expr['op']);
|
||||
tmp.push(' ');
|
||||
}
|
||||
return tmp.join('');
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<div class="alarm-expression">
|
||||
|
||||
<input id="expression"
|
||||
name="expression"
|
||||
type="hidden">
|
||||
|
||||
<div class="alert alert-warning"
|
||||
role="alert"
|
||||
ng-if="!vm.metrics.length"
|
||||
translate>
|
||||
No metric available
|
||||
</div>
|
||||
|
||||
<div class="container-fluid" ng-if="vm.metrics.length">
|
||||
|
||||
<div class="row center-block" ng-if="vm.expression">
|
||||
<pre class="text-primary expression-preview"
|
||||
ng-class="{'expression-valid': vm.expressionValid, 'expression-invalid': !vm.expressionValid}">{{ vm.expression }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="row"
|
||||
ng-repeat="expr in vm.subExpressions track by $id(expr)" ng-cloak>
|
||||
|
||||
<div class="container-fluid"
|
||||
style="padding-left:0px;padding-right:0px">
|
||||
<div class="row">
|
||||
<!-- :: one time binding -->
|
||||
|
||||
<mon-alarm-sub-expression metrics="vm.metrics"
|
||||
functions="::vm.functions"
|
||||
comparators="::vm.comparators"
|
||||
operators="::vm.operators"
|
||||
sub-expression="vm.subExpressions[$index]"
|
||||
preview="expr.preview"
|
||||
connectable="expr.connectable"
|
||||
class="alarm-sub-expression"
|
||||
ng-class="{'col-lg-11 col-md-11 col-sm-11 col-xs-11': !expr.preview, 'col-lg-9 col-md-9 col-sm-9 col-xs-9': expr.preview}"
|
||||
ng-cloak></mon-alarm-sub-expression>
|
||||
<div ng-class="{'col-lg-1 col-md-1 col-sm-1 col-xs-1': !expr.preview, 'col-lg-3 col-md-3 col-sm-3 col-xs-3': expr.preview}">
|
||||
|
||||
<button role="button"
|
||||
title="{$ 'Edit'|translate $}"
|
||||
class="btn btn-default btn-xs"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-if="expr.preview"
|
||||
ng-click="expr.preview = false">
|
||||
<span class="fa fa-edit" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button role="button"
|
||||
title="{$ 'Submit'|translate $}"
|
||||
class="btn btn-default btn-xs"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-if="!expr.preview"
|
||||
ng-disabled="!expr.$valid"
|
||||
ng-click="expr.preview = true">
|
||||
<span class="fa fa-check"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default btn-xs"
|
||||
role="button"
|
||||
title="{$ 'Up'|translate $}"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-disabled="$index === 0 || !expr.$valid"
|
||||
ng-click="vm.reorderExpression($event, $index, $index - 1)">
|
||||
<span class="fa fa-sort-asc"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs"
|
||||
role="button"
|
||||
title="{$ 'Add'|translate $}"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-disabled="!expr.$valid"
|
||||
ng-click="vm.addExpression($event, $index + 1)">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs"
|
||||
role="button"
|
||||
title="{$ 'Remove'|translate $}"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-disabled="vm.subExpressions.length === 1"
|
||||
ng-click="vm.removeExpression($event, $index)">
|
||||
<span class="fa fa-minus"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs"
|
||||
role="button"
|
||||
title="{$ 'Down'|translate $}"
|
||||
ng-class="{'btn-block': !expr.preview}"
|
||||
ng-disabled="$index === vm.subExpressions.length - 1 || !expr.$valid"
|
||||
ng-click="vm.reorderExpression($event, $index, $index + 1)">
|
||||
<span class="fa fa-sort-desc"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -0,0 +1,155 @@
|
||||
<div ng-if="vm.preview" class="sub-expression-preview">
|
||||
<span class="text-muted">{{ vm.model | monExpression:false }}</span>
|
||||
</div>
|
||||
|
||||
<form name="subExpressionForm" ng-if="!vm.preview" novalidate>
|
||||
|
||||
<div class="row expression-details" ng-if="vm.connectable">
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<label class="control-label" for="expressionOperator">
|
||||
<span class="field-label">{{ 'Operator'|translate }}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<select id="expressionOperator"
|
||||
name="operator"
|
||||
class="form-control input-sm"
|
||||
title="{$ 'Operators'|translate $}"
|
||||
aria-label="{$ 'Operators'|translate $}"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
required
|
||||
ng-change="vm.updateExpression()"
|
||||
ng-model="vm.model.op"
|
||||
ng-options="f[0] as f[1] for f in vm.operators"
|
||||
ng-class="{'has-error': subExpressionForm.operator.$invalid && !subExpressionForm.$pristine}"
|
||||
aria-describedby="helpBlock"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expression-details">
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<label class="control-label" for="expressionFunction">
|
||||
<span class="field-label">{{ 'Function'|translate }}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<select id="expressionFunction"
|
||||
name="function"
|
||||
class="form-control input-sm"
|
||||
aria-label="{$ 'Function'|translate $}"
|
||||
title="{$ 'Function'|translate $}"
|
||||
required
|
||||
ng-change="vm.updateExpression()"
|
||||
ng-model="vm.model.fun"
|
||||
ng-options="f[0] as f[1] for f in vm.functions"
|
||||
ng-class="{'has-error': subExpressionForm.function.$invalid && !subExpressionForm.$pristine}"
|
||||
aria-describedby="helpBlock"></select>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-6">
|
||||
<label class="control-label" for="expressionFunction">
|
||||
<span class="field-label">{{ 'Metric'|translate }}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<select id="expressionMetric"
|
||||
name="metric"
|
||||
class="form-control input-sm"
|
||||
aria-label="{$ 'Metric'|translate $}"
|
||||
title="{$ 'Metric'|translate $}"
|
||||
required
|
||||
ng-change="vm.resetExpression();vm.onMetricChanged(vm.model.metric)"
|
||||
ng-model="vm.model.metric"
|
||||
ng-options="metric.name for metric in vm.metrics|monUniqueMetric|orderBy:'name'"
|
||||
ng-class="{'has-error': subExpressionForm.metric.$invalid && !subExpressionForm.$pristine}"
|
||||
aria-describedby="helpBlock"></select>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<label class="control-label" for="expressionFunction">
|
||||
<span class="field-label">{{ 'Comparator'|translate }}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<select id="expressionComparator"
|
||||
name="comparator"
|
||||
class="form-control input-sm"
|
||||
title="{$ 'Comparator'|translate $}"
|
||||
aria-label="{$ 'Comparator'|translate $}"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
required
|
||||
ng-change="vm.updateExpression()"
|
||||
ng-model="vm.model.cmp"
|
||||
ng-options="f[0] as f[1] for f in vm.comparators"
|
||||
ng-class="{'has-error': subExpressionForm.comparator.$invalid && !subExpressionForm.$pristine}"
|
||||
aria-describedby="helpBlock"></select>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<label class="control-label" for="expressionFunction">
|
||||
<span class="field-label">{{ 'Threshold'|translate }}</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<input id="expressionThreshold"
|
||||
name="threshold"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control input-sm"
|
||||
aria-label="{$ 'Threshold'|translate $}"
|
||||
title="{$ 'Threshold'|translate $}"
|
||||
required
|
||||
ng-change="vm.updateExpression()"
|
||||
ng-model="vm.model.threshold"
|
||||
ng-class="{'has-error': subExpressionForm.threshold.$invalid && !subExpressionForm.$pristine}"
|
||||
aria-describedby="helpBlock"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expression-details">
|
||||
<div class="col-md-10 col-xs-10">
|
||||
<tags-input id="dimension-chooser"
|
||||
ng-model="vm.tags"
|
||||
placeholder="{$ 'Add a dimension'|translate $}"
|
||||
add-from-autocomplete-only="true"
|
||||
on-tag-added="vm.onDimensionsUpdated()"
|
||||
on-tag-removed="vm.onDimensionsUpdated()">
|
||||
<auto-complete source="vm.possibleDimensions($query)"
|
||||
max-results-to-show="30"
|
||||
min-length="1">
|
||||
</auto-complete>
|
||||
</tags-input>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-2">
|
||||
<div class="form-group">
|
||||
<label class="btn expression-deterministic"
|
||||
ng-class="{'btn-primary active': vm.model.deterministic, 'btn-default': !vm.model.deterministic}"
|
||||
ng-click="vm.model.deterministic = !vm.model.deterministic; vm.updateExpression()"
|
||||
translate>Deterministic</label>
|
||||
<input name="deterministic"
|
||||
type="hidden"
|
||||
ng-model="vm.model.deterministic">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- if window is small enough better hide this div to save some space -->
|
||||
<div class="row expression-details hidden-sm hidden-xs"
|
||||
ng-if="vm.matchingMetrics.length">
|
||||
<div class="topologyBalloon" id="metrics"
|
||||
style="position:static;display: block;">
|
||||
<div class="contentBody">
|
||||
<table class="detailInfoTable">
|
||||
<caption translate>Matching Metrics</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th translate>name</th>
|
||||
<th translate>dimensions</th>
|
||||
</tr>
|
||||
<tr ng-repeat="metric in vm.matchingMetrics track by $id(metric)">
|
||||
<td>{$ metric.name $}</td>
|
||||
<td style="white-space:normal">{$ metric.dimensions |
|
||||
json $}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
Loading…
x
Reference in New Issue
Block a user