Add Alarm Definition
Make changes to UI to differentiate between alarms and alarm definitions. At some point the backend of Monasca will change to support this and then we will use the new APIs. But for now, this exposes the user model but still uses the existing underlying alarm data model. Change-Id: I30c71be813f06bef06d13ae618d89114e450e305
This commit is contained in:
parent
a0fb0a5773
commit
0454625eaf
0
monitoring/alarmdefs/__init__.py
Normal file
0
monitoring/alarmdefs/__init__.py
Normal file
19
monitoring/alarmdefs/constants.py
Normal file
19
monitoring/alarmdefs/constants.py
Normal file
@ -0,0 +1,19 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
URL_PREFIX = 'horizon:monitoring:alarmdefs:'
|
||||
TEMPLATE_PREFIX = 'monitoring/alarmdefs/'
|
299
monitoring/alarmdefs/forms.py
Normal file
299
monitoring/alarmdefs/forms.py
Normal file
@ -0,0 +1,299 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import re
|
||||
|
||||
from django import forms as django_forms
|
||||
from django.template.loader import get_template
|
||||
from django.template import Context
|
||||
from django.utils import html
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from monitoring import api
|
||||
from monitoring.alarmdefs import constants
|
||||
|
||||
|
||||
class ExpressionWidget(forms.Widget):
|
||||
def __init__(self, initial, attrs):
|
||||
super(ExpressionWidget, self).__init__(attrs)
|
||||
self.initial = initial
|
||||
|
||||
def render(self, name, value, attrs):
|
||||
final_attrs = self.build_attrs(attrs, name=name)
|
||||
t = get_template(constants.TEMPLATE_PREFIX + 'expression_field.html')
|
||||
local_attrs = {'service': ''}
|
||||
local_attrs.update(final_attrs)
|
||||
context = Context(local_attrs)
|
||||
return t.render(context)
|
||||
|
||||
|
||||
|
||||
class SimpleExpressionWidget(django_forms.MultiWidget):
|
||||
def __init__(self, initial, attrs=None):
|
||||
comparators = [('>', '>'), ('>=', '>='), ('<', '<'), ('<=', '<=')]
|
||||
func = [('min', _('min')), ('max', _('max')), ('sum', _('sum')),
|
||||
('count', _('count')), ('avg', _('avg'))]
|
||||
_widgets = (
|
||||
django_forms.widgets.Select(attrs=attrs, choices=func),
|
||||
ExpressionWidget(initial, attrs={}),
|
||||
django_forms.widgets.Select(attrs=attrs, choices=comparators),
|
||||
django_forms.widgets.TextInput(),
|
||||
)
|
||||
super(SimpleExpressionWidget, self).__init__(_widgets, attrs)
|
||||
|
||||
def decompress(self, expr):
|
||||
if expr:
|
||||
return re.search('^(\w+)\((.*)\) ([<>=]*) (.*)$', expr).groups()
|
||||
else:
|
||||
return [None, None, None, None]
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return ''.join(rendered_widgets)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
values = [
|
||||
widget.value_from_datadict(data, files, name + '_%s' % i)
|
||||
for i, widget in enumerate(self.widgets)]
|
||||
try:
|
||||
expression = '%s(%s) %s %s' % (values[0],
|
||||
values[1],
|
||||
values[2],
|
||||
values[3])
|
||||
except ValueError:
|
||||
return ''
|
||||
else:
|
||||
return expression
|
||||
|
||||
|
||||
class NotificationField(forms.MultiValueField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NotificationField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _get_choices(self):
|
||||
return self._choices
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
def compress(self, data_list):
|
||||
return data_list
|
||||
|
||||
def clean(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class NotificationCreateWidget(forms.Select):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NotificationCreateWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
output = '<table id="notification_table" ' + \
|
||||
'class="table table-condensed">'
|
||||
output += '<thead><tr><th>%s</th></tr></thead>' % \
|
||||
unicode(_("Name"))
|
||||
if value:
|
||||
idx = 1
|
||||
for notification in value:
|
||||
output += '<tr><td>'
|
||||
output += ('<select id="id_notifications_%d" ' +
|
||||
'name="notifications_%d"> ') % (idx, idx)
|
||||
options = self.render_options(
|
||||
choices,
|
||||
[notification['id']])
|
||||
if options:
|
||||
output += options
|
||||
output += '</select>'
|
||||
output += '<td><a href="" id="remove_notif_button">X</a></td>'
|
||||
output += '</td></tr>'
|
||||
idx += 1
|
||||
else:
|
||||
output += '<tr><td>'
|
||||
output += '<select id="id_notifications_1" '
|
||||
output += 'name="notifications_1"> '
|
||||
options = self.render_options(choices, [value])
|
||||
if options:
|
||||
output += options
|
||||
output += '</select>'
|
||||
output += '<td><a href="" id="remove_notif_button">X</a></td>'
|
||||
output += '</td></tr>'
|
||||
output += '</table>'
|
||||
label = unicode(_("+ Add more"))
|
||||
output += '<a href="" id="add_notification_button">%s</a>' % (label)
|
||||
return html.format_html(output)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
notifications = []
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
notification_id = "%s_%d" % (name, i)
|
||||
if notification_id in data:
|
||||
if len(data[notification_id]) > 0:
|
||||
notifications.append({"id":
|
||||
data[notification_id]})
|
||||
else:
|
||||
break
|
||||
return notifications
|
||||
|
||||
|
||||
class BaseAlarmForm(forms.SelfHandlingForm):
|
||||
@classmethod
|
||||
def _instantiate(cls, request, *args, **kwargs):
|
||||
return cls(request, *args, **kwargs)
|
||||
|
||||
def _init_fields(self, readOnly=False, create=False, initial=None):
|
||||
required = True
|
||||
textWidget = None
|
||||
textAreaWidget = forms.Textarea(attrs={'class': 'large-text-area'})
|
||||
choiceWidget = forms.Select
|
||||
if create:
|
||||
expressionWidget = SimpleExpressionWidget(initial)
|
||||
notificationWidget = NotificationCreateWidget()
|
||||
else:
|
||||
expressionWidget = textAreaWidget
|
||||
notificationWidget = NotificationCreateWidget()
|
||||
|
||||
self.fields['name'] = forms.CharField(label=_("Name"),
|
||||
required=required,
|
||||
max_length=250,
|
||||
widget=textWidget)
|
||||
self.fields['expression'] = forms.CharField(label=_("Expression"),
|
||||
required=required,
|
||||
widget=expressionWidget)
|
||||
self.fields['description'] = forms.CharField(label=_("Description"),
|
||||
required=False,
|
||||
widget=textAreaWidget)
|
||||
sev_choices = [("LOW", _("Low")),
|
||||
("MEDIUM", _("Medium")),
|
||||
("HIGH", _("High")),
|
||||
("CRITICAL", _("Critical"))]
|
||||
self.fields['severity'] = forms.ChoiceField(label=_("Severity"),
|
||||
choices=sev_choices,
|
||||
widget=choiceWidget,
|
||||
required=False)
|
||||
self.fields['state'] = forms.CharField(label=_("State"),
|
||||
required=False,
|
||||
widget=textWidget)
|
||||
self.fields['actions_enabled'] = \
|
||||
forms.BooleanField(label=_("Notifications Enabled"),
|
||||
required=False,
|
||||
initial=True)
|
||||
self.fields['notifications'] = NotificationField(
|
||||
label=_("Notifications"),
|
||||
required=False,
|
||||
widget=notificationWidget)
|
||||
|
||||
def set_notification_choices(self, request):
|
||||
try:
|
||||
notifications = api.monitor.notification_list(request)
|
||||
except Exception as e:
|
||||
notifications = []
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve notifications: %s') % e)
|
||||
notification_choices = [(notification['id'], notification['name'])
|
||||
for notification in notifications]
|
||||
if notification_choices:
|
||||
if len(notification_choices) > 1:
|
||||
notification_choices.insert(
|
||||
0, ("", unicode(_("Select Notification"))))
|
||||
else:
|
||||
notification_choices.insert(
|
||||
0, ("", unicode(_("No notifications available."))))
|
||||
|
||||
self.fields['notifications'].choices = notification_choices
|
||||
|
||||
def clean_expression(self):
|
||||
data = self.cleaned_data['expression']
|
||||
value = data.split(' ')[2]
|
||||
if not value.isdigit():
|
||||
raise forms.ValidationError("Value must be an integer")
|
||||
|
||||
# Always return the cleaned data, whether you have changed it or
|
||||
# not.
|
||||
return data
|
||||
|
||||
|
||||
class CreateAlarmForm(BaseAlarmForm):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateAlarmForm, self).__init__(request, *args, **kwargs)
|
||||
super(CreateAlarmForm, self)._init_fields(readOnly=False, create=True,
|
||||
initial=kwargs['initial'])
|
||||
super(CreateAlarmForm, self).set_notification_choices(request)
|
||||
self.fields.pop('state')
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
alarm_actions = [notification.get('id')
|
||||
for notification in data['notifications']]
|
||||
api.monitor.alarm_create(
|
||||
request,
|
||||
name=data['name'],
|
||||
expression=data['expression'],
|
||||
description=data['description'],
|
||||
severity=data['severity'],
|
||||
alarm_actions=alarm_actions,
|
||||
ok_actions=alarm_actions,
|
||||
undetermined_actions=alarm_actions,
|
||||
)
|
||||
messages.success(request,
|
||||
_('Alarm Definition has been created successfully.'))
|
||||
except Exception as e:
|
||||
exceptions.handle(request, _('Unable to create the alarm definition: %s') % e.message)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class EditAlarmForm(BaseAlarmForm):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(EditAlarmForm, self).__init__(request, *args, **kwargs)
|
||||
super(EditAlarmForm, self)._init_fields(readOnly=False)
|
||||
super(EditAlarmForm, self).set_notification_choices(request)
|
||||
self.fields.pop('state')
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
alarm_actions = []
|
||||
if data['notifications']:
|
||||
alarm_actions = [notification.get('id')
|
||||
for notification in data['notifications']]
|
||||
api.monitor.alarm_update(
|
||||
request,
|
||||
alarm_id=self.initial['id'],
|
||||
state=self.initial['state'],
|
||||
severity=data['severity'],
|
||||
name=data['name'],
|
||||
expression=data['expression'],
|
||||
description=data['description'],
|
||||
actions_enabled=data['actions_enabled'],
|
||||
alarm_actions=alarm_actions,
|
||||
ok_actions=alarm_actions,
|
||||
undetermined_actions=alarm_actions,
|
||||
)
|
||||
messages.success(request,
|
||||
_('Alarm definition has been updated.'))
|
||||
except Exception as e:
|
||||
exceptions.handle(request, _('Unable to edit the alarm definition: %s') % e)
|
||||
return False
|
||||
return True
|
29
monitoring/alarmdefs/panel.py
Normal file
29
monitoring/alarmdefs/panel.py
Normal file
@ -0,0 +1,29 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
import horizon
|
||||
|
||||
from monitoring import dashboard
|
||||
|
||||
|
||||
class AlarmDefinitions(horizon.Panel):
|
||||
name = _("Alarm Definitions")
|
||||
slug = 'alarmdefs'
|
||||
|
||||
|
||||
dashboard.Monitoring.register(AlarmDefinitions)
|
134
monitoring/alarmdefs/tables.py
Normal file
134
monitoring/alarmdefs/tables.py
Normal file
@ -0,0 +1,134 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from monitoring.alarmdefs import constants
|
||||
from monitoring import api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def show_by_dimension(data, dim_name):
|
||||
if 'dimensions' in data['expression_data']:
|
||||
dimensions = data['expression_data']['dimensions']
|
||||
if dim_name in dimensions:
|
||||
return str(data['expression_data']['dimensions'][dim_name])
|
||||
return ""
|
||||
|
||||
|
||||
class CreateAlarm(tables.LinkAction):
|
||||
name = "create_alarm"
|
||||
verbose_name = _("Create Alarm Definition")
|
||||
classes = ("ajax-modal",)
|
||||
icon = "plus"
|
||||
policy_rules = (("alarm", "alarm:create"),)
|
||||
ajax = True
|
||||
|
||||
|
||||
def get_link_url(self):
|
||||
return urlresolvers.reverse(constants.URL_PREFIX + 'alarm_create',
|
||||
args=())
|
||||
|
||||
def allowed(self, request, datum=None):
|
||||
return True
|
||||
|
||||
|
||||
class EditAlarm(tables.LinkAction):
|
||||
name = "edit_alarm"
|
||||
verbose_name = _("Edit Alarm Definition")
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
|
||||
def get_link_url(self, datum):
|
||||
return urlresolvers.reverse(constants.URL_PREFIX + 'alarm_edit',
|
||||
args=(
|
||||
datum['id'], ))
|
||||
|
||||
def allowed(self, request, datum=None):
|
||||
return True
|
||||
|
||||
|
||||
class GraphMetric(tables.LinkAction):
|
||||
name = "graph_alarm"
|
||||
verbose_name = _("Graph Metric")
|
||||
icon = "dashboard"
|
||||
|
||||
def render(self):
|
||||
self.attrs['target'] = 'newtab'
|
||||
return super(self, GraphMetric).render()
|
||||
|
||||
def get_link_url(self, datum):
|
||||
name = datum['expression_data']['metric_name']
|
||||
threshold = datum['expression_data']['threshold']
|
||||
self.attrs['target'] = '_blank'
|
||||
return "/static/grafana/index.html#/dashboard/script/detail.js?token=%s&name=%s&threshold=%s" % \
|
||||
(self.table.request.user.token.id, name, threshold)
|
||||
|
||||
def allowed(self, request, datum=None):
|
||||
return 'metric_name' in datum['expression_data']
|
||||
|
||||
|
||||
class DeleteAlarm(tables.DeleteAction):
|
||||
name = "delete_alarm"
|
||||
verbose_name = _("Delete Alarm Definition")
|
||||
data_type_singular = _("Alarm Definition")
|
||||
data_type_plural = _("Alarm Definitions")
|
||||
|
||||
def allowed(self, request, datum=None):
|
||||
return True
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
api.monitor.alarm_delete(request, obj_id)
|
||||
|
||||
|
||||
class AlarmsFilterAction(tables.FilterAction):
|
||||
def filter(self, table, alarms, filter_string):
|
||||
"""Naive case-insensitive search."""
|
||||
q = filter_string.lower()
|
||||
return [alarm for alarm in alarms
|
||||
if q in alarm.name.lower()]
|
||||
|
||||
|
||||
class AlarmsTable(tables.DataTable):
|
||||
target = tables.Column('name', verbose_name=_('Name'),
|
||||
link=constants.URL_PREFIX + 'alarm_detail',
|
||||
)
|
||||
description = tables.Column('description', verbose_name=_('Description'))
|
||||
enabled = tables.Column('actions_enabled',
|
||||
verbose_name=_('Notifications Enabled'))
|
||||
|
||||
def get_object_id(self, obj):
|
||||
return obj['id']
|
||||
|
||||
def get_object_display(self, obj):
|
||||
return obj['name']
|
||||
|
||||
class Meta:
|
||||
name = "alarms"
|
||||
verbose_name = _("Alarm Definitions")
|
||||
row_actions = (GraphMetric,
|
||||
EditAlarm,
|
||||
DeleteAlarm,
|
||||
)
|
||||
table_actions = (CreateAlarm,
|
||||
AlarmsFilterAction,
|
||||
DeleteAlarm,
|
||||
)
|
56
monitoring/alarmdefs/templates/alarmdefs/_create.html
Normal file
56
monitoring/alarmdefs/templates/alarmdefs/_create.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}create_alarm_form{% endblock %}
|
||||
{% block form_action %}{{ action_url }}{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Create Alarm" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% blocktrans %}
|
||||
The Name field is used to identify the notification method.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}
|
||||
The Expression field which if true, triggers a notification to be sent.
|
||||
See <a href="http://docs.hpcloud.com/api/v13/monitoring/#AlarmExpressions-jumplink-span" target="_blank">Alarm Expressions</a> for how to write an expression.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}
|
||||
The Alarm Actions field contains the list of Notification that should be sent when transitioning to an ALARM state.
|
||||
{% endblocktrans %}</p>
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$('#add_notification_button').click(function(){
|
||||
num = $('#notification_table tr').length;
|
||||
id = "id_notifications_" + num;
|
||||
name = "notifications_" + num;
|
||||
options = $("#id_notifications_" + (num - 1)).html();
|
||||
options = options.replace('selected="selected"','');
|
||||
row = '<tr><td><select id="' + id +'" name="' + name + '">' + options + '</select></td><td><a href="" id="remove_notif_button">X</a></td></tr>'
|
||||
$('#notification_table tr:last').after(row);
|
||||
return false;
|
||||
});
|
||||
$('#notification_table').on('click', '#remove_notif_button', (function(event){
|
||||
var target = $(event.target.parentNode.parentNode);
|
||||
target.remove();
|
||||
return false;
|
||||
}));
|
||||
metricsList = {{ metrics | safe }}
|
||||
</script>
|
||||
<style>
|
||||
#id_expression_2 {
|
||||
width: 60px;
|
||||
}
|
||||
</style>
|
||||
<link href='{{ STATIC_URL }}/monitoring/css/ng-tags-input.css' type="text/css" rel="stylesheet"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Alarm" %}" />
|
||||
<a href="{{ cancel_url }}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
39
monitoring/alarmdefs/templates/alarmdefs/_detail.html
Normal file
39
monitoring/alarmdefs/templates/alarmdefs/_detail.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
<div class="info row detail">
|
||||
<h4>{% trans "Info" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ alarm.name|default:_("None") }}</dd>
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ alarm.description }}</dd>
|
||||
<dt>{% trans "Expression" %}</dt>
|
||||
<dd>{{ alarm.expression }}</dd>
|
||||
<dt>{% trans "Severity" %}</dt>
|
||||
<dd>{{ alarm.severity }}</dd>
|
||||
<dt>{% trans "Notifications Enabled" %}</dt>
|
||||
<dd>{{ alarm.actions_enabled }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="specs row detail">
|
||||
<h4>{% trans "Notifications" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Address" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for row in alarm.notifications %}
|
||||
<tr>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.type }}</td>
|
||||
<td>{{ row.address }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
50
monitoring/alarmdefs/templates/alarmdefs/_edit.html
Normal file
50
monitoring/alarmdefs/templates/alarmdefs/_edit.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}edit_alarm_form{% endblock %}
|
||||
{% block form_action %}{{ action_url }}{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Edit Alarm Definition" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% blocktrans %}
|
||||
The Name field is used to identify the notification method.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}
|
||||
The Expression field which if true, triggers a notification to be sent.
|
||||
See <a href="http://docs.hpcloud.com/api/v13/monitoring/#AlarmExpressions-jumplink-span" target="_blank">Alarm Expressions</a> for how to write an expression.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}
|
||||
The Alarm Actions field contains the list of Notification that should be sent when transitioning to an ALARM state.
|
||||
{% endblocktrans %}</p>
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$('#add_notification_button').click(function(){
|
||||
num = $('#notification_table tr').length;
|
||||
id = "id_notifications_" + num;
|
||||
name = "notifications_" + num;
|
||||
options = $("#id_notifications_" + (num - 1)).html();
|
||||
options = options.replace('selected="selected"','');
|
||||
row = '<tr><td><select id="' + id +'" name="' + name + '">' + options + '</select></td><td><a href="" id="remove_notif_button">X</a></td></tr>'
|
||||
$('#notification_table tr:last').after(row);
|
||||
return false;
|
||||
});
|
||||
$('#notification_table').on('click', '#remove_notif_button', (function(event){
|
||||
var target = $(event.target.parentNode.parentNode);
|
||||
target.remove();
|
||||
return false;
|
||||
}));
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
|
||||
<a href="{{ cancel_url }}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
13
monitoring/alarmdefs/templates/alarmdefs/alarm.html
Normal file
13
monitoring/alarmdefs/templates/alarmdefs/alarm.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block title %}{% trans 'Alarm Definitions' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Alarm Definitions" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
30
monitoring/alarmdefs/templates/alarmdefs/alarm_history.html
Normal file
30
monitoring/alarmdefs/templates/alarmdefs/alarm_history.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block title %}{% trans 'Alarm History' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Alarm History" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block manage_overview %}
|
||||
{% block page_title %}
|
||||
<div class="page_title table_header">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="{% url 'horizon:noc_tools:service_health:index' %}" class="showspinner">
|
||||
{% trans "Service Health" %}</a> : {{ region_name }} - >
|
||||
{% trans "Service :" %} {{ service }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="table_actions">
|
||||
<a href="{% url 'horizon:noc_tools:service_health:index' %}" class="close showspinner">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_title %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
||||
|
52
monitoring/alarmdefs/templates/alarmdefs/alarm_meter.html
Normal file
52
monitoring/alarmdefs/templates/alarmdefs/alarm_meter.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block title %}{% trans 'Measurements for Alarms' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Measurements that Triggered Alarm" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block manage_overview %}
|
||||
{% block page_title %}
|
||||
<div class="page_title table_header">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="{% url 'horizon:noc_tools:service_health:index' %}" class="showspinner">
|
||||
{% trans "Service Health" %}</a> : {{ region_name }} - >
|
||||
{% trans "Service :" %} {{ service }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="table_actions">
|
||||
<a href="{% url 'horizon:noc_tools:service_health:index' %}" class="close showspinner">×</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_title %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
||||
<script type="text/javascript">
|
||||
google.load("visualization", "1", {packages:["corechart"]});
|
||||
google.setOnLoadCallback(drawChart);
|
||||
function drawChart() {
|
||||
var data = google.visualization.arrayToDataTable([
|
||||
['Time', 'Response Time', 'Threshold'],
|
||||
['8:42', 1000, 1200],
|
||||
['8:43', 1170, 1200],
|
||||
['8:44', 1190, 1200],
|
||||
['8:45', 1230, 1200]
|
||||
]);
|
||||
|
||||
var options = {
|
||||
title: 'API Response Time'
|
||||
};
|
||||
|
||||
var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
|
||||
chart.draw(data, options);
|
||||
}
|
||||
</script>
|
||||
<div id="chart_div" style="width: 900px; height: 500px;"></div>
|
||||
|
||||
{% endblock %}
|
||||
|
11
monitoring/alarmdefs/templates/alarmdefs/create.html
Normal file
11
monitoring/alarmdefs/templates/alarmdefs/create.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans 'Create Alarm Definition' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Create Alarm Definition" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'monitoring/alarmdefs/_create.html' %}
|
||||
{% endblock %}
|
11
monitoring/alarmdefs/templates/alarmdefs/detail.html
Normal file
11
monitoring/alarmdefs/templates/alarmdefs/detail.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans 'Alarm Definition Details' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Alarm Definition Details" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'monitoring/alarmdefs/_detail.html' %}
|
||||
{% endblock %}
|
11
monitoring/alarmdefs/templates/alarmdefs/edit.html
Normal file
11
monitoring/alarmdefs/templates/alarmdefs/edit.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans 'Edit Alarm Definitions' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title="Edit Alarm Definitions" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'monitoring/alarmdefs/_edit.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,31 @@
|
||||
{% load i18n %}
|
||||
<div ng-controller="alarmEditController" ng-init="init('{{ service }}')">
|
||||
<input type="hidden" name="{{ name }}" id="dimension"/>
|
||||
<select id="metric-chooser" class="form-control"
|
||||
ng-model="currentMetric"
|
||||
ng-options="metric for metric in metricNames"
|
||||
ng-change="metricChanged()"></select>
|
||||
<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()"
|
||||
max-results-to-show="30" min-length="1">
|
||||
</auto-complete>
|
||||
</tags-input>
|
||||
<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">{$metric[dim]$}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
52
monitoring/alarmdefs/tests.py
Normal file
52
monitoring/alarmdefs/tests.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.core import urlresolvers
|
||||
from mock import patch, call # noqa
|
||||
|
||||
from monitoring.test import helpers
|
||||
from monitoring.alarmdefs import constants
|
||||
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
constants.URL_PREFIX + 'index')
|
||||
CREATE_URL = urlresolvers.reverse(
|
||||
constants.URL_PREFIX + 'alarm_create', args=())
|
||||
DETAIL_URL = urlresolvers.reverse(
|
||||
constants.URL_PREFIX + 'alarm_detail', args=('12345',))
|
||||
|
||||
|
||||
class AlarmDefinitionsTest(helpers.TestCase):
|
||||
def test_alarmdefs_get(self):
|
||||
with patch('monitoring.api.monitor', **{
|
||||
'spec_set': ['alarm_list'],
|
||||
'alarm_list.return_value': [],
|
||||
}) as mock:
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertEqual(mock.alarm_list.call_count, 1)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'monitoring/alarmdefs/alarm.html')
|
||||
|
||||
def test_alarmdefs_create(self):
|
||||
with patch('monitoring.api.monitor', **{
|
||||
'spec_set': ['notification_list', 'metrics_list'],
|
||||
'notification_list.return_value': [],
|
||||
'metrics_list.return_value': [],
|
||||
}) as mock:
|
||||
res = self.client.get(CREATE_URL)
|
||||
self.assertEqual(mock.notification_list.call_count, 1)
|
||||
self.assertEqual(mock.metrics_list.call_count, 1)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'monitoring/alarmdefs/_create.html')
|
||||
|
||||
def test_alarmdefs_detail(self):
|
||||
with patch('monitoring.api.monitor', **{
|
||||
'spec_set': ['alarm_get'],
|
||||
'alarm_get.return_value': {
|
||||
'alarm_actions': []
|
||||
}
|
||||
}) as mock:
|
||||
res = self.client.get(DETAIL_URL)
|
||||
self.assertEqual(mock.alarm_get.call_count, 1)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'monitoring/alarmdefs/_detail.html')
|
33
monitoring/alarmdefs/urls.py
Normal file
33
monitoring/alarmdefs/urls.py
Normal file
@ -0,0 +1,33 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
from django.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from monitoring.alarmdefs import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^alarm/create$',
|
||||
views.AlarmCreateView.as_view(),
|
||||
name='alarm_create'),
|
||||
url(r'^(?P<id>[^/]+)/alarm_detail/$',
|
||||
views.AlarmDetailView.as_view(),
|
||||
name='alarm_detail'),
|
||||
url(r'^alarm/(?P<id>[^/]+)/alarm_edit/$',
|
||||
views.AlarmEditView.as_view(),
|
||||
name='alarm_edit'),
|
||||
)
|
190
monitoring/alarmdefs/views.py
Normal file
190
monitoring/alarmdefs/views.py
Normal file
@ -0,0 +1,190 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.import logging
|
||||
|
||||
import json
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy, reverse # noqa
|
||||
from django.template import defaultfilters as filters
|
||||
from django.utils.translation import ugettext as _ # noqa
|
||||
from django.views.generic import View # noqa
|
||||
from django.views.generic import TemplateView # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
import monascaclient.exc as exc
|
||||
from monitoring.alarmdefs import constants
|
||||
from monitoring.alarmdefs import forms as alarm_forms
|
||||
from monitoring.alarmdefs import tables as alarm_tables
|
||||
from monitoring import api
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = alarm_tables.AlarmsTable
|
||||
template_name = constants.TEMPLATE_PREFIX + 'alarm.html'
|
||||
|
||||
def get_data(self):
|
||||
results = []
|
||||
try:
|
||||
results = api.monitor.alarm_list(self.request)
|
||||
except Exception:
|
||||
messages.error(self.request, _("Could not retrieve alarm definitions"))
|
||||
return results
|
||||
|
||||
|
||||
class AlarmCreateView(forms.ModalFormView):
|
||||
form_class = alarm_forms.CreateAlarmForm
|
||||
template_name = constants.TEMPLATE_PREFIX + 'create.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AlarmCreateView, self).get_context_data(**kwargs)
|
||||
context["cancel_url"] = self.get_success_url()
|
||||
context["action_url"] = reverse(constants.URL_PREFIX + 'alarm_create',
|
||||
args=())
|
||||
metrics = api.monitor.metrics_list(self.request)
|
||||
|
||||
context["metrics"] = json.dumps(metrics)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy(constants.URL_PREFIX + 'index',
|
||||
args=())
|
||||
|
||||
|
||||
def transform_alarm_data(obj):
|
||||
return obj
|
||||
return {'id': getattr(obj, 'id', None),
|
||||
'name': getattr(obj, 'name', None),
|
||||
'expression': getattr(obj, 'expression', None),
|
||||
'state': filters.title(getattr(obj, 'state', None)),
|
||||
'severity': filters.title(getattr(obj, 'severity', None)),
|
||||
'actions_enabled': filters.title(getattr(obj, 'actions_enabled',
|
||||
None)),
|
||||
'notifications': getattr(obj, 'alarm_actions', None), }
|
||||
|
||||
|
||||
def transform_alarm_history(results, name):
|
||||
newlist = []
|
||||
for item in results:
|
||||
temp = {}
|
||||
temp['alarm_id'] = item['alarm_id']
|
||||
temp['name'] = name
|
||||
temp['old_state'] = item['old_state']
|
||||
temp['new_state'] = item['new_state']
|
||||
temp['timestamp'] = item['timestamp']
|
||||
temp['reason'] = item['reason']
|
||||
temp['reason_data'] = item['reason_data']
|
||||
newlist.append(temp)
|
||||
return newlist
|
||||
|
||||
|
||||
class AlarmDetailView(TemplateView):
|
||||
template_name = constants.TEMPLATE_PREFIX + 'detail.html'
|
||||
|
||||
def get_object(self):
|
||||
id = self.kwargs['id']
|
||||
try:
|
||||
if hasattr(self, "_object"):
|
||||
return self._object
|
||||
self._object = None
|
||||
self._object = api.monitor.alarm_get(self.request, id)
|
||||
notifications = []
|
||||
# Fetch the notification object for each alarm_actions
|
||||
for id in self._object["alarm_actions"]:
|
||||
try:
|
||||
notification = api.monitor.notification_get(
|
||||
self.request,
|
||||
id)
|
||||
notifications.append(notification)
|
||||
# except exceptions.NOT_FOUND:
|
||||
except exc.HTTPException:
|
||||
msg = _("Notification %s has already been deleted.") % id
|
||||
notifications.append({"id": id,
|
||||
"name": unicode(msg),
|
||||
"type": "",
|
||||
"address": ""})
|
||||
self._object["notifications"] = notifications
|
||||
return self._object
|
||||
except Exception:
|
||||
redirect = self.get_success_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve alarm details.'),
|
||||
redirect=redirect)
|
||||
return None
|
||||
|
||||
def get_initial(self):
|
||||
self.alarm = self.get_object()
|
||||
return transform_alarm_data(self.alarm)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AlarmDetailView, self).get_context_data(**kwargs)
|
||||
self.get_initial()
|
||||
context["alarm"] = self.alarm
|
||||
context["cancel_url"] = self.get_success_url()
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy(constants.URL_PREFIX + 'index')
|
||||
|
||||
|
||||
class AlarmEditView(forms.ModalFormView):
|
||||
form_class = alarm_forms.EditAlarmForm
|
||||
template_name = constants.TEMPLATE_PREFIX + 'edit.html'
|
||||
|
||||
def get_object(self):
|
||||
id = self.kwargs['id']
|
||||
try:
|
||||
if hasattr(self, "_object"):
|
||||
return self._object
|
||||
self._object = None
|
||||
self._object = api.monitor.alarm_get(self.request, id)
|
||||
notifications = []
|
||||
# Fetch the notification object for each alarm_actions
|
||||
for id in self._object["alarm_actions"]:
|
||||
try:
|
||||
notification = api.monitor.notification_get(
|
||||
self.request,
|
||||
id)
|
||||
notifications.append(notification)
|
||||
# except exceptions.NOT_FOUND:
|
||||
except exc.HTTPException:
|
||||
msg = _("Notification %s has already been deleted.") % id
|
||||
messages.warning(self.request, msg)
|
||||
self._object["notifications"] = notifications
|
||||
return self._object
|
||||
except Exception:
|
||||
redirect = self.get_success_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve alarm details.'),
|
||||
redirect=redirect)
|
||||
return None
|
||||
|
||||
def get_initial(self):
|
||||
self.alarm = self.get_object()
|
||||
return transform_alarm_data(self.alarm)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AlarmEditView, self).get_context_data(**kwargs)
|
||||
id = self.kwargs['id']
|
||||
context["cancel_url"] = self.get_success_url()
|
||||
context["action_url"] = reverse(constants.URL_PREFIX + 'alarm_edit',
|
||||
args=(id,))
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy(constants.URL_PREFIX + 'index',
|
||||
args=())
|
@ -173,8 +173,6 @@ class AlarmsTable(tables.DataTable):
|
||||
host = tables.Column(transform=show_host, verbose_name=_('Host'))
|
||||
service = tables.Column(transform=show_service, verbose_name=_('Service'))
|
||||
state = tables.Column('state', verbose_name=_('State'))
|
||||
enabled = tables.Column('actions_enabled',
|
||||
verbose_name=_('Notifications Enabled'))
|
||||
|
||||
def get_object_id(self, obj):
|
||||
return obj['id']
|
||||
@ -187,10 +185,9 @@ class AlarmsTable(tables.DataTable):
|
||||
verbose_name = _("Alarms")
|
||||
row_actions = (GraphMetric,
|
||||
ShowAlarmHistory,
|
||||
EditAlarm,
|
||||
DeleteAlarm,
|
||||
)
|
||||
table_actions = (CreateAlarm,
|
||||
table_actions = (
|
||||
AlarmsFilterAction,
|
||||
DeleteAlarm,
|
||||
)
|
||||
|
@ -22,7 +22,7 @@ import horizon
|
||||
class Monitoring(horizon.Dashboard):
|
||||
name = _("Monitoring")
|
||||
slug = "monitoring"
|
||||
panels = ('overview', 'alarms', 'notifications')
|
||||
panels = ('overview', 'alarmdefs', 'alarms', 'notifications',)
|
||||
default_panel = 'overview'
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user