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:
Tomasz Trębski 2016-06-22 12:33:27 +02:00
parent a181c1cc34
commit 58367e3e00
19 changed files with 909 additions and 317 deletions

View File

@ -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,

View File

@ -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=())

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"')

View File

@ -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):

View File

@ -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 = {

View File

@ -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;
}

View 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);
}
}
}
}

View File

@ -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);
}

View File

@ -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(', ');
}
});

View 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();
}
}
}

View 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;
}
}

View 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('');
}
}

View File

@ -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>

View File

@ -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>