Vitrage - entity graph fixes:

=============================
1. Circles on entity graph are bigger
2. Alarms fixed - now shown on graph
3. Heat Stack icon added
4. Captions in entities info panel fixed.
5. Info Panel style fixed
6. Heat stack filter added
7. Heat stack - Depth of graph added
8. Bug - data change was checked just by number of nodes. Now it also check names

Change-Id: I4af0863a5e217ec3435dccce3b75d1a7e05ac4e0
This commit is contained in:
Alon Heller 2017-09-13 12:37:49 +03:00
parent 8be5a32a61
commit e3caa58f24
13 changed files with 544 additions and 340 deletions

View File

@ -12,6 +12,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""
https://docs.openstack.org/horizon/latest/contributor/tutorials/plugin.html
"""
""" This file will likely be necessary if creating a Django or Angular driven
plugin. This file is intended to act as a convenient location for
interacting with the new service this plugin is supporting.
While interactions with the service can be handled in the views.py,
isolating the logic is an established pattern in Horizon.
"""
from horizon.utils.memoized import memoized # noqa from horizon.utils.memoized import memoized # noqa
from keystoneauth1.identity.generic.token import Token from keystoneauth1.identity.generic.token import Token
@ -19,6 +29,9 @@ from keystoneauth1.session import Session
from openstack_dashboard.api import base from openstack_dashboard.api import base
from vitrageclient import client as vitrage_client from vitrageclient import client as vitrage_client
import logging
LOG = logging.getLogger(__name__)
@memoized @memoized
def vitrageclient(request, password=None): def vitrageclient(request, password=None):
@ -30,10 +43,19 @@ def vitrageclient(request, password=None):
return vitrage_client.Client('1', session) return vitrage_client.Client('1', session)
def topology(request, query=None, graph_type='tree', all_tenants='false'): def topology(request, query=None, graph_type='tree', all_tenants='false',
root=None, limit=None):
LOG.info("--------- CALLING VITRAGE_CLIENT ---request %s", str(request))
LOG.info("--------- CALLING VITRAGE_CLIENT ---query %s", str(query))
LOG.info("------ CALLING VITRAGE_CLIENT --graph_type %s", str(graph_type))
LOG.info("---- CALLING VITRAGE_CLIENT --all_tenants %s", str(all_tenants))
LOG.info("--------- CALLING VITRAGE_CLIENT --------root %s", str(root))
LOG.info("--------- CALLING VITRAGE_CLIENT --------limit %s", str(limit))
return vitrageclient(request).topology.get(query=query, return vitrageclient(request).topology.get(query=query,
graph_type=graph_type, graph_type=graph_type,
all_tenants=all_tenants) all_tenants=all_tenants,
root=root,
limit=limit)
def alarms(request, vitrage_id='all', all_tenants='false'): def alarms(request, vitrage_id='all', all_tenants='false'):

View File

@ -12,16 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from django.views import generic from django.views import generic
import logging
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.api.rest import urls from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
from vitrage_dashboard.api import vitrage from vitrage_dashboard.api import vitrage
LOG = logging.getLogger(__name__)
@urls.register @urls.register
class Topolgy(generic.View): class Topolgy(generic.View):
@ -41,16 +39,27 @@ class Topolgy(generic.View):
""" """
''' original default is graph ''' ''' original default is graph '''
LOG.info("--------- reques --------------- %s", str(request))
graph_type = 'tree' graph_type = 'tree'
all_tenants = 'false' all_tenants = 'false'
root = None
limit = None
if 'graph_type' in request.GET: if 'graph_type' in request.GET:
graph_type = request.GET.get('graph_type') graph_type = request.GET.get('graph_type')
if 'all_tenants' in request.GET: if 'all_tenants' in request.GET:
all_tenants = request.GET.get('all_tenants') all_tenants = request.GET.get('all_tenants')
if 'root' in request.GET:
root = request.GET.get('root')
if 'depth' in request.GET:
limit = int(request.GET.get('depth'))
query = None query = None
if 'query' in request.GET: if 'query' in request.GET:
query = request.GET.get('query') query = request.GET.get('query')
LOG.info("--A request QUERY -- %s", str(query))
elif graph_type == 'tree': elif graph_type == 'tree':
''' Default tree query - get computes, used by Sunburst''' ''' Default tree query - get computes, used by Sunburst'''
query = '{"and": [{"==": {"vitrage_category": "RESOURCE"}},' \ query = '{"and": [{"==": {"vitrage_category": "RESOURCE"}},' \
@ -64,7 +73,9 @@ class Topolgy(generic.View):
return vitrage.topology(request=request, return vitrage.topology(request=request,
query=query, query=query,
graph_type=graph_type, graph_type=graph_type,
all_tenants=all_tenants) all_tenants=all_tenants,
root=root,
limit=limit)
@urls.register @urls.register

View File

@ -30,12 +30,13 @@
config = config || {}; config = config || {};
if (graph_type) { if (graph_type) {
config.params = {graph_type: graph_type}; config.params = config.params || {};
config.params.graph_type = graph_type;
} }
if (admin){ if (admin){
(!config.params) ? config.params = {all_tenants: true} : config.params.all_tenants = true; (!config.params) ? config.params = {all_tenants: true} : config.params.all_tenants = true;
} }
console.info('CONFIG in core - ', config)
return apiService.get('/api/vitrage/topology/', config) return apiService.get('/api/vitrage/topology/', config)
.error(function () { .error(function () {
toastService.add('error', gettext('Unable to fetch the Vitrage Topology service.')); toastService.add('error', gettext('Unable to fetch the Vitrage Topology service.'));

View File

@ -5,15 +5,17 @@
.module('horizon.dashboard.project.vitrage') .module('horizon.dashboard.project.vitrage')
.controller('EntitiesController', EntitiesController); .controller('EntitiesController', EntitiesController);
EntitiesController.$inject = ['$scope', 'vitrageTopologySrv', '$interval', '$location']; EntitiesController.$inject = ['$scope', 'vitrageTopologySrv', '$interval', '$location', '$timeout'];
function EntitiesController($scope, vitrageTopologySrv, $interval, $location) { function EntitiesController($scope, vitrageTopologySrv, $interval, $location, $timeout) {
this.model = {selected: {}}; this.model = {selected: {}};
var _this = this, var _this = this,
loadTime = 5000, loadTime = 5000,
errorCount = 0, errorCount = 0,
loadInterval; loadInterval,
initialized = false,
timeoutSubscriber;
$scope.$watch('automaticRefresh', function (newData, oldData) { $scope.$watch('automaticRefresh', function (newData, oldData) {
@ -25,6 +27,26 @@
} }
}); });
$scope.$watch('selectedHeat', function (newData, oldData) {
if (newData !== undefined && newData != oldData) {
if (timeoutSubscriber) {
console.log('Canceling heat stack timeout');
$timeout.cancel(timeoutSubscriber);
}
timeoutSubscriber = $timeout(loadData, 250);
}
});
$scope.$watch('depthRange', function (newData, oldData) {
if (newData !== undefined && newData != oldData) {
if (timeoutSubscriber) {
console.log('Canceling depth timeout');
$timeout.cancel(timeoutSubscriber);
}
timeoutSubscriber = $timeout(loadData, 250);
}
});
$scope.$on('graphItemClicked', function (event, data) { $scope.$on('graphItemClicked', function (event, data) {
_this.selectedItem = data; _this.selectedItem = data;
event.stopPropagation(); event.stopPropagation();
@ -40,10 +62,34 @@
function loadData() { function loadData() {
var url = $location.absUrl(); var url = $location.absUrl();
var admin = false; var admin = false;
if (url.indexOf('admin') != -1) admin = true; if (url.indexOf('admin') != -1) admin = true;
// get heat stacks
if (!_this.initialized) {
vitrageTopologySrv.getTopology('graph', null, admin) vitrageTopologySrv.getTopology('graph', null, admin)
.then(function (res) {
var heats = [];
_.each(res.data.nodes, function (node) {
if (node.vitrage_type === 'heat.stack') {
heats.push({name: node.name, vitrageId: node.vitrage_id});
}
});
$scope.heats = heats;
_this.initialized = true;
});
}
var config = {params: {query: '{"==": {"vitrage_is_deleted" : false} }'}};
if ($scope.selectedHeat && $scope.selectedHeat !== 'all') {
config.params.depth = $scope.depthRange;
config.params.root = $scope.selectedHeat;
}
console.log('Config: ', config);
vitrageTopologySrv.getTopology('graph', config, admin)
.then(function (res) { .then(function (res) {
var nodes = res.data.nodes, var nodes = res.data.nodes,
links = res.data.links; links = res.data.links;
@ -65,7 +111,7 @@
}) })
.catch(function (res) { .catch(function (res) {
nextLoad(++errorCount * 2 * loadTime); nextLoad(++errorCount * 2 * loadTime);
}) });
} }
function nextLoad(mill) { function nextLoad(mill) {
@ -82,11 +128,35 @@
$interval.cancel(loadInterval); $interval.cancel(loadInterval);
} }
function dataChanged(nodes1, nodes2, links1, links2) {
if (nodes1.length !== nodes2.length || links1.length !== links2.length) {
console.log('number of nodes or links changed');
return true;
}
// check for nodes change
for (var i = 0; i < nodes1.length; i++) {
var nodeFound = false;
for (var j = 0; j < nodes2.length; j++) {
if (nodes1[i].name === nodes2[j].name) {
nodeFound = true;
continue;
}
}
if (!nodeFound) {
console.log('name of nodes changed');
return true;
}
}
return false;
}
function mergeData(data) { function mergeData(data) {
var graphNodes = $scope.vm.graphData.nodes, var graphNodes = $scope.vm.graphData.nodes,
graphLinks = $scope.vm.graphData.links; graphLinks = $scope.vm.graphData.links;
if (graphNodes.length != data.nodes.length || graphLinks.length != data.links.length) { if (dataChanged(graphNodes, data.nodes, graphLinks, data.links)) {
graphNodes.splice(0, graphNodes.length); graphNodes.splice(0, graphNodes.length);
graphLinks.splice(0, graphLinks.length); graphLinks.splice(0, graphLinks.length);
@ -137,6 +207,7 @@
return n.id == d.id; return n.id == d.id;
}); });
if (reald) {
var state = reald.vitrage_operational_state; var state = reald.vitrage_operational_state;
if (state) { if (state) {
switch (state.toLowerCase()) { switch (state.toLowerCase()) {
@ -152,6 +223,7 @@
} }
} }
} }
}
return cls; return cls;
}) })
.style('font-size', function (d) { .style('font-size', function (d) {
@ -186,9 +258,9 @@
.style('stroke', function (d) { .style('stroke', function (d) {
var category = d.category; var category = d.category;
if (category && category.toLowerCase() === 'alarm') { if (category && category.toLowerCase() === 'alarm') {
return '18px' return '18px';
} }
return '20px' return '20px';
}) })
.classed('icon', true) .classed('icon', true)
.classed('fill-only', function (d) { .classed('fill-only', function (d) {
@ -204,6 +276,7 @@
icon = '\uf0f3'; //\uf0a2'; //bell-o icon = '\uf0f3'; //\uf0a2'; //bell-o
} else { } else {
var type = d.vitrage_type || 'no_type'; var type = d.vitrage_type || 'no_type';
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'nova.instance': case 'nova.instance':
icon = '\uf108'; //fa-desktop icon = '\uf108'; //fa-desktop
@ -226,12 +299,16 @@
case 'openstack.cluster': case 'openstack.cluster':
icon = '\uf0c2'; //fa-cloud icon = '\uf0c2'; //fa-cloud
break; break;
case 'heat.stack':
icon = '\uf1b3'; //fa-cubes
break;
default: default:
icon = '\uf013'; //fa-cog console.warn('Vitrage type not supported: ' + d.vitrage_type);
icon = '\uf1b3'; //fa-cog
break; break;
} }
} }
return icon return icon;
}); });
} }

View File

@ -7,7 +7,11 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<hz-entities-info item="vm.selectedItem"></hz-entities-info> <hz-entities-info item="vm.selectedItem"></hz-entities-info>
<hz-entities-toolbox search-text="searchText" auto-refresh="automaticRefresh"></hz-entities-toolbox> <hz-entities-toolbox search-text="searchText"
auto-refresh="automaticRefresh"
heats="heats"
selected-heat="selectedHeat"
depth-range="depthRange"></hz-entities-toolbox>
<hz-entities-graph data="vm.graphData" <hz-entities-graph data="vm.graphData"
selected="vm.model.selected" selected="vm.model.selected"
search-item="searchText" search-item="searchText"

View File

@ -23,7 +23,7 @@ function hzEntitiesGraph() {
var minZoom = 0.3, var minZoom = 0.3,
maxZoom = 4, maxZoom = 4,
linkWidth = 1, linkWidth = 1,
circleRadius = 14, circleRadius = 18,
circlePadding = 1, circlePadding = 1,
zoom = d3.behavior.zoom().scaleExtent([minZoom, maxZoom]), zoom = d3.behavior.zoom().scaleExtent([minZoom, maxZoom]),
ellipsisWidth = 80, ellipsisWidth = 80,
@ -361,7 +361,7 @@ function hzEntitiesGraph() {
.attr('dominant-baseline', 'central') .attr('dominant-baseline', 'central')
.attr('transform', 'scale(1)') .attr('transform', 'scale(1)')
.attr('class', function(d) { .attr('class', function(d) {
var category = d.category, var category = d.vitrage_category,
cls = ''; cls = '';
if (category && category.toLowerCase() === 'alarm') { if (category && category.toLowerCase() === 'alarm') {
@ -407,7 +407,7 @@ function hzEntitiesGraph() {
return cls; return cls;
}) })
.style('font-size', function(d) { .style('font-size', function(d) {
var category = d.category || 'no_category', var category = d.vitrage_category || 'no_category',
icon_size; icon_size;
if (category && category.toLowerCase() === 'alarm') { if (category && category.toLowerCase() === 'alarm') {
@ -437,11 +437,11 @@ function hzEntitiesGraph() {
return icon_size; return icon_size;
}) })
.style('stroke', function(d) { .style('stroke', function(d) {
var category = d.category; var category = d.vitrage_category;
if (category && category.toLowerCase() === 'alarm') { if (category && category.toLowerCase() === 'alarm') {
return '18px' return '18px';
} }
return '20px' return '20px';
}) })
.classed('icon', true) .classed('icon', true)
.classed('fill-only', function(d) { .classed('fill-only', function(d) {
@ -451,7 +451,7 @@ function hzEntitiesGraph() {
} }
}) })
.text(function(d) { .text(function(d) {
var category = d.category, var category = d.vitrage_category,
icon; icon;
if (category && category.toLowerCase() === 'alarm') { if (category && category.toLowerCase() === 'alarm') {
@ -480,12 +480,15 @@ function hzEntitiesGraph() {
case 'openstack.cluster': case 'openstack.cluster':
icon = '\uf0c2'; //fa-cloud icon = '\uf0c2'; //fa-cloud
break; break;
case 'heat.stack':
icon = '\uf1b3'; //fa-cubes
break;
default: default:
icon = '\uf013'; //fa-cog icon = '\uf013'; //fa-cog
break; break;
} }
} }
return icon return icon;
}); });
content.append('text') content.append('text')
@ -493,7 +496,7 @@ function hzEntitiesGraph() {
.attr('dx', '-18px') .attr('dx', '-18px')
.attr('dy', '-12px') .attr('dy', '-12px')
.text('\uf08d') .text('\uf08d')
.on('click', pinNode) .on('click', pinNode);
var textNode = content.append('text') var textNode = content.append('text')
.classed('.label', true) .classed('.label', true)
@ -510,7 +513,7 @@ function hzEntitiesGraph() {
} }
d.width = 2 * (circleRadius + circlePadding) + (d.bbox ? d.bbox.width * 2 : 0); d.width = 2 * (circleRadius + circlePadding) + (d.bbox ? d.bbox.width * 2 : 0);
d.height = 2 * (circleRadius + circlePadding); d.height = 2 * (circleRadius + circlePadding);
}) });
}) })
.append('title') .append('title')
.text(function(d) { return d.name; }); .text(function(d) { return d.name; });
@ -576,7 +579,7 @@ function hzEntitiesGraph() {
svg_g.selectAll('.link').classed('selected', function(d) { svg_g.selectAll('.link').classed('selected', function(d) {
return d.source.high && d.target.high; return d.source.high && d.target.high;
}) });
} }
function selectNone(d) { function selectNone(d) {
@ -697,7 +700,7 @@ function hzEntitiesGraph() {
findNodes(node, depth, allNodes, linksMap); findNodes(node, depth, allNodes, linksMap);
} else if (depth <= -1) { } else if (depth <= -1) {
//Always find 'depth' + alarms & (sdns + alarms) //Always find 'depth' + alarms & (sdns + alarms)
if (node.category.toLowerCase() === 'alarm') { if (node && node.vitrage_category && node.vitrage_category.toLowerCase() === 'alarm') {
node.high = true; node.high = true;
node.highDepth = 0; node.highDepth = 0;
} else if (!node.high && node.vitrage_type && node.vitrage_type.toLowerCase() === 'sdn_controller') { } else if (!node.high && node.vitrage_type && node.vitrage_type.toLowerCase() === 'sdn_controller') {

View File

@ -16,5 +16,55 @@ function hzEntitiesInfo() {
function link(scope, element, attrs) { function link(scope, element, attrs) {
scope.blackList = ['name', 'vitrage_is_deleted', 'vitrage_is_placeholder', 'index', 'graph_index', scope.blackList = ['name', 'vitrage_is_deleted', 'vitrage_is_placeholder', 'index', 'graph_index',
'fixed', 'weight', 'px', 'py', 'x', 'y', 'width', 'height', 'bbox', 'high', 'highDepth']; 'fixed', 'weight', 'px', 'py', 'x', 'y', 'width', 'height', 'bbox', 'high', 'highDepth'];
scope.parseItem = {};
// TODO: Order info by this priority
var topPriority = ['Vitrage resource type', 'Vitrage operational severity', 'Vitrage category', 'vitrage_aggregated_severity', 'vitrage_type', 'vitrage_operational_state']
scope.$watch('item', function (newData, oldData) {
if (newData !== undefined && newData != oldData) {
var tmpItem = copyObject(scope.item);
var itemParsed = {};
// 1. Replace _ with spaces
// 2. First letter uppercase
for (var property in tmpItem) {
if (scope.blackList.indexOf(property) < 0) {
if (tmpItem.hasOwnProperty(property)) {
var parsedProperty = '';
parsedProperty= property.split("_").join(" ");
parsedProperty = parsedProperty.charAt(0).toUpperCase() + parsedProperty.substr(1).toLowerCase();
itemParsed[parsedProperty] = tmpItem[property];
} }
} }
}
scope.parseItem = itemParsed;
}
});
function copyObject(orig, deep) {
// 1. copy has same prototype as orig
var copy = Object.create(Object.getPrototypeOf(orig));
// 2. copy has all of origs properties
copyOwnPropertiesFrom(copy, orig, deep);
return copy;
}
function copyOwnPropertiesFrom(target, source, deep) {
Object.getOwnPropertyNames(source)
.forEach(function(propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
if (deep && typeof desc.value === 'object') {
target[propKey] = copyObject(source[propKey], deep);
}
});
return target;
}
}
}

View File

@ -1,10 +1,10 @@
<div class="entities-info" ng-show="item"> <div class="entities-info" ng-show="item">
<div class="info clearfix" ng-if="item.name"> <div class="info clearfix" ng-if="parseItem.name">
<span class="key pull-left ellipsis" title="{$ 'name' | translate $}">{$ 'name' | translate $}: </span> <span class="key pull-left ellipsis font-title" title="{$ 'name' | translate $}">{$ 'name' | translate $}: </span>
<span class="value name pull-left ellipsis" title="{$ item.name $}"> {$ item.name $}</span> <span class="value name pull-left ellipsis" title="{$ parseItem.name $}"> {$ parseItem.name $}</span>
</div> </div>
<div class="info clearfix" ng-if="blackList.indexOf(key) < 0" ng-repeat="(key, value) in item"> <div class="info clearfix" ng-repeat="(key, value) in parseItem">
<span class="key pull-left ellipsis" title="{$ key | translate $}">{$ key | translate $}: </span> <span class="key pull-left ellipsis keys-font font-title" title="{$ key | translate $}">{$ key | translate $}: </span>
<span class="value pull-left ellipsis" title="{$ value $}"> {$ value $}</span> <span class="value pull-left ellipsis font-title" title="{$ value $}"> {$ value $}</span>
</div> </div>
</div> </div>

View File

@ -2,13 +2,18 @@
position: absolute; position: absolute;
margin: 12px; margin: 12px;
padding:6px 10px; padding:6px 10px;
width: 240px; width: 446px;
min-height: 120px; min-height: 120px;
max-height: 400px; max-height: 400px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
opacity: 0.7;
&:hover {
opacity: 1;
}
.info { .info {
margin: 0 0 1px 0; margin: 0 0 1px 0;
@ -27,4 +32,12 @@
} }
} }
.keys-font {
color: #428bca;
font-style: italic;
}
.font-title {
font-size: 12px;
}
} }

View File

@ -12,12 +12,17 @@ function hzEntitiesToolbox($rootScope) {
scope: { scope: {
item: '=', item: '=',
searchText: '=', searchText: '=',
autoRefresh: '=' autoRefresh: '=',
heats: '=',
selectedHeat: '=',
depthRange: '='
} }
}; };
return directive; return directive;
function link(scope, element, attrs) { function link(scope, element, attrs) {
scope.selectedHeat = 'all';
scope.depthRange = 2;
scope.autoRefresh = !!horizon.cookies.get('entitiesAutomaticRefresh'); scope.autoRefresh = !!horizon.cookies.get('entitiesAutomaticRefresh');
console.log('Getting autoRefresh cookie: ', scope.autoRefresh); console.log('Getting autoRefresh cookie: ', scope.autoRefresh);
@ -25,6 +30,6 @@ function hzEntitiesToolbox($rootScope) {
scope.broadcast = function(event) { scope.broadcast = function(event) {
console.log('click', event); console.log('click', event);
$rootScope.$broadcast('toolbox-' + event); $rootScope.$broadcast('toolbox-' + event);
} };
} }
} }

View File

@ -1,5 +1,17 @@
<div class="entities-toolbox"> <div class="entities-toolbox">
<div class="input-group input-group-xs heats-filter">
<select class="form-control" ng-model="selectedHeat">
<option value="all">All Heat Stacks</option>
<option ng-repeat="heat in heats track by $index" ng-value="heat.vitrageId">{{heat.name}}</option>
</select>
</div>
<div class="input-group input-group-xs heats-filter" ng-show="selectedHeat !== 'all'">
<label translate>Detail Level</label>
<input type="range" min="1" max="4" ng-value="4" ng-model="depthRange" class="slider">
</div>
<div class="input-group input-group-xs"> <div class="input-group input-group-xs">
<span class="input-group-addon search-icon"> <span class="input-group-addon search-icon">
<span class="fa fa-search"></span> <span class="fa fa-search"></span>

View File

@ -33,4 +33,9 @@
padding-left: 5px; padding-left: 5px;
padding-top: 2px; padding-top: 2px;
} }
.heats-filter {
width: 100%;
padding-bottom: 10px;
}
} }

View File

@ -14,6 +14,7 @@
vitrageAPI = $injector.get('horizon.app.core.openstack-service-api.vitrage'); vitrageAPI = $injector.get('horizon.app.core.openstack-service-api.vitrage');
} }
function getTopology(graph_type, config, admin) { function getTopology(graph_type, config, admin) {
if (vitrageAPI) { if (vitrageAPI) {
@ -24,7 +25,7 @@
.error(function (err) { .error(function (err) {
console.error(err); console.error(err);
} }
) );
} }
} }
@ -38,7 +39,7 @@
.error(function (err) { .error(function (err) {
console.error(err); console.error(err);
} }
) );
} }
} }
@ -52,7 +53,7 @@
.error(function (err) { .error(function (err) {
console.error(err); console.error(err);
} }
) );
} }
} }
@ -66,7 +67,7 @@
.error(function (err) { .error(function (err) {
console.error(err); console.error(err);
} }
) );
} }
} }
@ -75,6 +76,6 @@
getAlarms: getAlarms, getAlarms: getAlarms,
getRootCauseAnalysis: getRootCauseAnalysis, getRootCauseAnalysis: getRootCauseAnalysis,
getTemplates: getTemplates getTemplates: getTemplates
} };
} }
})(); })();