Merge pull request #1839 from coreos-inc/better-notifications
Better notifications UI and features
This commit is contained in:
commit
349bd1e0fa
16 changed files with 644 additions and 263 deletions
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
|
||||
from datetime import datetime
|
||||
from notificationhelper import build_event_data
|
||||
|
@ -138,7 +139,28 @@ class VulnerabilityFoundEvent(NotificationEvent):
|
|||
', '.join(event_data['tags']))
|
||||
|
||||
|
||||
class BuildQueueEvent(NotificationEvent):
|
||||
class BaseBuildEvent(NotificationEvent):
|
||||
def should_perform(self, event_data, notification_data):
|
||||
event_config = json.loads(notification_data.event_config_json)
|
||||
ref_regex = event_config.get('ref-regex') or None
|
||||
if ref_regex is None:
|
||||
return True
|
||||
|
||||
# Lookup the ref. If none, this is a non-git build and we should not fire the event.
|
||||
ref = event_data.get('trigger_metadata', {}).get('ref', None)
|
||||
if ref is None:
|
||||
return False
|
||||
|
||||
# Try parsing the regex string as a regular expression. If we fail, we fail to fire
|
||||
# the event.
|
||||
try:
|
||||
return bool(re.compile(str(ref_regex)).match(ref))
|
||||
except Exception:
|
||||
logger.warning('Regular expression error for build event filter: %s', ref_regex)
|
||||
return False
|
||||
|
||||
|
||||
class BuildQueueEvent(BaseBuildEvent):
|
||||
@classmethod
|
||||
def event_name(cls):
|
||||
return 'build_queued'
|
||||
|
@ -177,7 +199,7 @@ class BuildQueueEvent(NotificationEvent):
|
|||
return 'Build queued ' + _build_summary(event_data)
|
||||
|
||||
|
||||
class BuildStartEvent(NotificationEvent):
|
||||
class BuildStartEvent(BaseBuildEvent):
|
||||
@classmethod
|
||||
def event_name(cls):
|
||||
return 'build_start'
|
||||
|
@ -205,7 +227,7 @@ class BuildStartEvent(NotificationEvent):
|
|||
return 'Build started ' + _build_summary(event_data)
|
||||
|
||||
|
||||
class BuildSuccessEvent(NotificationEvent):
|
||||
class BuildSuccessEvent(BaseBuildEvent):
|
||||
@classmethod
|
||||
def event_name(cls):
|
||||
return 'build_success'
|
||||
|
@ -234,7 +256,7 @@ class BuildSuccessEvent(NotificationEvent):
|
|||
return 'Build succeeded ' + _build_summary(event_data)
|
||||
|
||||
|
||||
class BuildFailureEvent(NotificationEvent):
|
||||
class BuildFailureEvent(BaseBuildEvent):
|
||||
@classmethod
|
||||
def event_name(cls):
|
||||
return 'build_failure'
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
#createNotificationModal .dropdown-select {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#createNotificationModal .options-table {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#createNotificationModal .options-table td {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
#createNotificationModal .options-table td.name {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#createNotificationModal .options-table-wrapper {
|
||||
padding: 10px;
|
||||
}
|
69
static/css/directives/ui/create-external-notification.css
Normal file
69
static/css/directives/ui/create-external-notification.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
.create-external-notification-element {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .dropdown-select {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .button-bar {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .options-table .section-header {
|
||||
font-size: 18px;
|
||||
padding: 6px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .options-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-external-notification-element .options-table td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .options-table td.name {
|
||||
padding-left: 20px;
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .options-table td.value {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .help-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-external-notification-element .help-table > tbody > tr > td {
|
||||
vertical-align: top;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.create-external-notification-element .help-table td.help-col {
|
||||
vertical-align: top;
|
||||
padding: 20px;
|
||||
padding-top: 40px;
|
||||
width: 500px;
|
||||
font-size: 15px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.create-external-notification-element .config-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
.create-external-notification-element .help-text {
|
||||
margin-top: 10px;
|
||||
color: #aaa;
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
<!-- Modal message dialog -->
|
||||
<div class="co-dialog modal fade" id="createNotificationModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="createForm" name="createForm" ng-submit="createNotification()">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-disabled="creating">×</button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-bell"></i> Create Repository Notification
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Creating spinner -->
|
||||
<div class="cor-loader" ng-show="status == 'creating' || status == 'authorizing-email'"></div>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'authorizing-email-sent'">
|
||||
An e-mail has been sent to <code>{{ currentConfig.email }}</code>. Please click the link contained
|
||||
in the e-mail.
|
||||
<br><br>
|
||||
<span class="cor-loader-inline"></span>
|
||||
</div>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'unauthorized-email'">
|
||||
The e-mail address <code>{{ currentConfig.email }}</code> has not been authorized to receive
|
||||
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||
the authorization process.
|
||||
</div>
|
||||
|
||||
<!-- Create View -->
|
||||
<div class="options-table-wrapper">
|
||||
<table class="options-table" ng-show="status == ''">
|
||||
<tr>
|
||||
<td class="name">Notification title:</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" placeholder="(Optional Title)" ng-model="currentTitle">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="options-table" ng-show="status == ''">
|
||||
<tr>
|
||||
<td class="name">When this occurs:</td>
|
||||
<td>
|
||||
<div class="dropdown-select" placeholder="'(Notification Event)'" selected-item="currentEvent.title"
|
||||
handle-item-selected="handleEventSelected(datum)" clear-value="clearCounter">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon fa fa-lg" ng-class="currentEvent.icon"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="event in events">
|
||||
<a ng-click="setEvent(event)">
|
||||
<i class="fa fa-lg" ng-class="event.icon"></i> {{ event.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="field in currentEvent.fields">
|
||||
<td class="name" valign="top">With {{ field.title }} of:</td>
|
||||
<td>
|
||||
<div ng-switch on="field.type">
|
||||
<select class="form-control" ng-if="field.type == 'enum'"
|
||||
ng-model="currentEventConfig[field.name]" required>
|
||||
<option ng-repeat="(key, info) in field.values | orderObjectBy: 'index'" value="{{key}}">{{ info.title }}</option>
|
||||
</select>
|
||||
|
||||
<div class="co-alert co-alert-info"
|
||||
style="margin-top: 6px; margin-bottom: 10px;"
|
||||
ng-if="field.values[currentEventConfig[field.name]].description">
|
||||
{{ field.values[currentEventConfig[field.name]].description }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="options-table" ng-show="status == ''">
|
||||
<tr>
|
||||
<td class="name">Then issue a:</td>
|
||||
<td>
|
||||
<div class="dropdown-select" placeholder="'(Notification Action)'" selected-item="currentMethod.title"
|
||||
handle-item-selected="handleMethodSelected(datum)" clear-value="clearCounter">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon fa fa-lg" ng-class="currentMethod.icon"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="method in methods">
|
||||
<a ng-click="setMethod(method)">
|
||||
<i class="fa fa-lg" ng-class="method.icon"></i> {{ method.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="field in currentMethod.fields">
|
||||
<td valign="top" class="name">{{ field.title }}:</td>
|
||||
<td>
|
||||
<div ng-switch on="field.type">
|
||||
<span ng-switch-when="email">
|
||||
<input type="email" class="form-control" ng-model="currentConfig[field.name]" required>
|
||||
</span>
|
||||
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" required>
|
||||
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
|
||||
<!-- TODO(jschorr): unify the ability to create an input box with all the usual features -->
|
||||
<div ng-switch-when="regex">
|
||||
<input type="text" class="form-control" ng-model="currentConfig[field.name]"
|
||||
ng-pattern="getPattern(field)"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
ng-name="field.name"
|
||||
id="{{ field.name }}"
|
||||
required>
|
||||
|
||||
<div class="alert alert-warning" style="margin-top: 10px; margin-bottom: 10px"
|
||||
ng-if="field.regex_fail_message && hasRegexMismatch(createForm.$error, field.name)">
|
||||
<span ng-bind-html="field.regex_fail_message"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-search" namespace="repository.namespace"
|
||||
placeholder="''"
|
||||
current-entity="currentConfig[field.name]"
|
||||
ng-model="currentConfig[field.name]"
|
||||
allowed-entities="['user', 'team', 'org']"
|
||||
ng-switch-when="entity"></div>
|
||||
|
||||
<div ng-if="getHelpUrl(field, currentConfig)"
|
||||
style="margin-top: 10px; margin-bottom: 10px">
|
||||
See: <a href="{{ getHelpUrl(field, currentConfig) }}" ng-safenewtab>{{ getHelpUrl(field, currentConfig) }}</a>
|
||||
</div>
|
||||
|
||||
<div class="co-alert co-alert-info" ng-if="currentMethod.id == 'webhook'"
|
||||
style="margin-top: 6px; margin-bottom: 0px">
|
||||
JSON metadata representing the event will be <b>POST</b>ed to the URL.
|
||||
<br><br>
|
||||
The contents for each event can be found in the user guide:
|
||||
<a href="http://docs.quay.io/guides/notifications.html#webhook{{ currentEvent.id ? '_' + currentEvent.id : '' }}"
|
||||
ng-safenewtab>
|
||||
http://docs.quay.io/guides/notifications.html
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth e-mail button bar -->
|
||||
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||
Send Authorization E-mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-disabled="creating">Cancel</button>
|
||||
</div>
|
||||
|
||||
<!-- Normal button bar -->
|
||||
<div class="modal-footer" ng-if="status == '' || status == 'creating'">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
||||
Create Notification
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-disabled="creating">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
233
static/directives/create-external-notification.html
Normal file
233
static/directives/create-external-notification.html
Normal file
|
@ -0,0 +1,233 @@
|
|||
<div class="create-external-notification-element">
|
||||
<form id="createForm" name="createForm" ng-submit="createNotification()">
|
||||
<!-- Creating spinner -->
|
||||
<div class="cor-loader" ng-show="status == 'creating'"></div>
|
||||
|
||||
<!-- Event -->
|
||||
<table class="help-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table class="options-table">
|
||||
<tr><td class="section-header" colspan="2">When this event occurs</td></tr>
|
||||
<tr>
|
||||
<td class="span-col" colspan="2">
|
||||
<div class="dropdown-select" placeholder="'Please select the event'" selected-item="currentEvent.title"
|
||||
handle-item-selected="handleEventSelected(datum)" clear-value="clearCounter">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon fa fa-lg" ng-class="currentEvent.icon"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="event in events">
|
||||
<a ng-click="setEvent(event)">
|
||||
<i class="fa fa-lg" ng-class="event.icon"></i> {{ event.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="field in currentEvent.fields">
|
||||
<td class="name" valign="top">With {{ field.title }} <span ng-if="field.optional">(optional)</span>:</td>
|
||||
<td class="value">
|
||||
<div ng-switch on="field.type">
|
||||
<!-- Enum -->
|
||||
<select class="form-control" ng-if="field.type == 'enum'"
|
||||
ng-model="currentEventConfig[field.name]" required>
|
||||
<option ng-repeat="(key, info) in field.values | orderObjectBy: 'index'" value="{{key}}">{{ info.title }}</option>
|
||||
</select>
|
||||
|
||||
<!-- Regular expression -->
|
||||
<div ng-switch-when="regex">
|
||||
<div class="regex-editor" placeholder="{{field.placeholder || '' }}"
|
||||
binding="currentConfig[field.name]" optional="field.optional"></div>
|
||||
</div>
|
||||
|
||||
<!-- Value description -->
|
||||
<div class="co-alert co-alert-info"
|
||||
style="margin-top: 6px; margin-bottom: 10px;"
|
||||
ng-if="field.values[currentEventConfig[field.name]].description">
|
||||
{{ field.values[currentEventConfig[field.name]].description }}
|
||||
</div>
|
||||
|
||||
<!-- Help text -->
|
||||
<div class="help-text" ng-if="field.help_text">
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="help-col hidden-sm hidden-xs">
|
||||
<span class="registry-name"></span> supports a number of events around repositories (such as push completed), building (build queued, build completed, etc) and security (vulnerability detected). Some events also allow for filtering, for further granular control of when notifications fire.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="config-section" ng-show="currentEvent">
|
||||
<!-- Notification -->
|
||||
<table class="help-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table class="options-table">
|
||||
<tr><td class="section-header" colspan="2">Then issue a notification</td></tr>
|
||||
<tr>
|
||||
<td class="span-col" colspan="2">
|
||||
<div class="dropdown-select" placeholder="'Please select a notification method'" selected-item="currentMethod.title"
|
||||
handle-item-selected="handleMethodSelected(datum)" clear-value="clearCounter">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon fa fa-lg" ng-class="currentMethod.icon"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="method in methods">
|
||||
<a ng-click="setMethod(method)">
|
||||
<i class="fa fa-lg" ng-class="method.icon"></i> {{ method.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="field in currentMethod.fields">
|
||||
<td valign="top" class="name">{{ field.title }}:</td>
|
||||
<td class="value">
|
||||
<div ng-switch on="field.type">
|
||||
<!-- Email -->
|
||||
<span ng-switch-when="email">
|
||||
<input type="email" class="form-control" ng-model="currentConfig[field.name]" ng-required="!field.optional">
|
||||
</span>
|
||||
|
||||
<!-- URL -->
|
||||
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" ng-required="!field.optional">
|
||||
|
||||
<!-- String -->
|
||||
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" ng-required="!field.optional">
|
||||
|
||||
<!-- Pattern (regex match) -->
|
||||
<div ng-switch-when="pattern">
|
||||
<input type="text" class="form-control" ng-model="currentConfig[field.name]"
|
||||
ng-pattern="getPattern(field)"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
ng-name="field.name"
|
||||
id="{{ field.name }}"
|
||||
ng-required="!field.optional">
|
||||
|
||||
<div class="alert alert-warning" style="margin-top: 10px; margin-bottom: 10px"
|
||||
ng-if="field.pattern_fail_message && hasRegexMismatch(createForm.$error, field.name)">
|
||||
<span ng-bind-html="field.pattern_fail_message"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entity -->
|
||||
<div class="entity-search" namespace="repository.namespace"
|
||||
placeholder="''"
|
||||
current-entity="currentConfig[field.name]"
|
||||
ng-model="currentConfig[field.name]"
|
||||
allowed-entities="['user', 'team', 'org']"
|
||||
ng-switch-when="entity"></div>
|
||||
|
||||
<div ng-if="getHelpUrl(field, currentConfig)"
|
||||
style="margin-top: 10px; margin-bottom: 10px">
|
||||
See: <a href="{{ getHelpUrl(field, currentConfig) }}" ng-safenewtab>{{ getHelpUrl(field, currentConfig) }}</a>
|
||||
</div>
|
||||
|
||||
<div class="help-text" ng-if="currentMethod.id == 'webhook'"
|
||||
style="margin-top: 6px; margin-bottom: 0px">
|
||||
JSON metadata representing the event will be <b>POST</b>ed to the URL. All requests made to TLS-enabled URLs will be signed with the <span class="registry-name"></span> key.
|
||||
<br><br>
|
||||
The contents for each event can be found in the user guide:
|
||||
<a href="http://docs.quay.io/guides/notifications.html#webhook{{ currentEvent.id ? '_' + currentEvent.id : '' }}"
|
||||
ng-safenewtab>
|
||||
http://docs.quay.io/guides/notifications.html
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help text -->
|
||||
<div class="help-text" ng-if="field.help_text">
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="help-col hidden-sm hidden-xs">
|
||||
Once an event has fired, <span class="registry-name"></span> supports a number of notification methods, including various chat systems (Slack, HipChat, etc), notification via e-mail or programatic handling via the firing of a webhook.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="config-section" ng-show="currentMethod">
|
||||
<table class="help-table">
|
||||
<tr>
|
||||
<td>
|
||||
<table class="options-table">
|
||||
<tr><td class="section-header" colspan="2">With extra configuration</td></tr>
|
||||
<tr>
|
||||
<td class="name">Notification title:</td>
|
||||
<td class="value">
|
||||
<input class="form-control" type="text" placeholder="Enter an optional title" ng-model="currentTitle">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="help-col hidden-sm hidden-xs">
|
||||
The title for a notification is an optional field for a human-readable title for the notification.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="button-bar">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
||||
Create Notification
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Authorize email dialog -->
|
||||
<div class="modal" tabindex="-1" role="dialog" id="authorizeEmailModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">E-mail authorization</h4>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" style="padding: 4px; padding-left: 20px;">
|
||||
<button type="button" class="close" ng-click="cancelEmailAuth()" style="padding: 4px;">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'authorizing-email-sent'">
|
||||
An e-mail has been sent to <code>{{ currentConfig.email }}</code>. Please click the link contained
|
||||
in the e-mail.
|
||||
<br><br>
|
||||
<span class="cor-loader-inline"></span>
|
||||
</div>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'unauthorized-email'">
|
||||
The e-mail address <code>{{ currentConfig.email }}</code> has not been authorized to receive
|
||||
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||
the authorization process.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth e-mail button bar -->
|
||||
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||
Send Authorization E-mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="cancelEmailAuth()" ng-disabled="creating">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
4
static/directives/regex-editor.html
Normal file
4
static/directives/regex-editor.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div class="regex-editor-element">
|
||||
<input name="regexField" type="text" class="form-control" ng-model="binding" placeholder="{{ placeholder || '' }}"
|
||||
ng-required="!optional" require-valid-regex>
|
||||
</div>
|
|
@ -75,14 +75,6 @@
|
|||
<img class="lock" src="/static/img/lock.svg">
|
||||
<img class="scan" src="/static/img/scan.svg">
|
||||
</span>
|
||||
|
||||
<b>Automated Security Scanning</b>
|
||||
<div>Continually scanning this repository for 17K+ known vulnerabilities. <a href="https://blog.quay.io/security-scanning-beta" ng-safenewtab>Read more about this feature</a>.</div>
|
||||
<div class="configure-alerts" ng-if="!hasEvents">
|
||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=settings&add_event=vulnerability_found"><i class="fa fa-bell-o"></i>Configure Vulnerability Alerts</a>
|
||||
</div>
|
||||
<div class="repository-events-summary" is-enabled="repository.can_admin" repository="repository"
|
||||
event-filter="vulnerability_found" has-events="hasEvents"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
<i class="fa fa-bell"></i> Events and Notifications
|
||||
|
||||
<div class="heading-controls hidden-sm hidden-xs">
|
||||
<button class="btn btn-primary" ng-click="askCreateNotification()">
|
||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/create-notification" class="btn btn-primary">
|
||||
<i class="fa fa-plus"></i> Create Notification
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
@ -19,7 +19,7 @@
|
|||
Click the "Create Notification" button above to add a new notification for a repository event.
|
||||
</div>
|
||||
<div class="empty-secondary-msg visible-sm visible-xs" ng-if="repository.can_write">
|
||||
<a ng-click="askCreateNotification()">Click here</a> to add a new notification for a repository event.
|
||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/create-notification">Click here</a> to add a new notification for a repository event.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -95,11 +95,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New notification dialog-->
|
||||
<div class="create-external-notification-dialog"
|
||||
repository="repository"
|
||||
counter="showNewNotificationCounter"
|
||||
default-data="newNotificationData"
|
||||
notification-created="handleNotificationCreated(notification)"></div>
|
||||
</div>
|
||||
|
|
|
@ -120,6 +120,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
|
|||
// Repo Build View
|
||||
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
|
||||
|
||||
// Create repository notification
|
||||
.route('/repository/:namespace/:name/create-notification', 'create-repository-notification')
|
||||
|
||||
// Repo List
|
||||
.route('/repository/', 'repo-list')
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
/**
|
||||
* An element which displays a dialog to register a new external notification on a repository.
|
||||
* An element which displays a form to register a new external notification on a repository.
|
||||
*/
|
||||
angular.module('quay').directive('createExternalNotificationDialog', function () {
|
||||
angular.module('quay').directive('createExternalNotification', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-external-notification-dialog.html',
|
||||
templateUrl: '/static/directives/create-external-notification.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'counter': '=counter',
|
||||
'notificationCreated': '¬ificationCreated',
|
||||
'defaultData': '=defaultData'
|
||||
},
|
||||
|
@ -27,7 +26,12 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
|||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||
|
||||
$scope.getPattern = function(field) {
|
||||
return new RegExp(field.regex);
|
||||
if (field._cached_regex) {
|
||||
return field._cached_regex;
|
||||
}
|
||||
|
||||
field._cached_regex = new RegExp(field.pattern);
|
||||
return field._cached_regex;
|
||||
};
|
||||
|
||||
$scope.setEvent = function(event) {
|
||||
|
@ -99,14 +103,6 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
|||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||
$scope.status = '';
|
||||
$scope.notificationCreated({'notification': resp});
|
||||
|
||||
// Used by repository-events-summary.
|
||||
if (!$scope.repository._notificationCounter) {
|
||||
$scope.repository._notificationCounter = 0;
|
||||
}
|
||||
|
||||
$scope.repository._notificationCounter++;
|
||||
$('#createNotificationModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -123,6 +119,7 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
|||
}
|
||||
|
||||
$scope.unauthorizedEmail = true;
|
||||
$('#authorizeEmailModal').modal({});
|
||||
};
|
||||
|
||||
$scope.sendAuthEmail = function() {
|
||||
|
@ -146,6 +143,11 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
|||
}, 1000);
|
||||
};
|
||||
|
||||
$scope.cancelEmailAuth = function() {
|
||||
$scope.status = '';
|
||||
$('#authorizeEmailModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.getHelpUrl = function(field, config) {
|
||||
var helpUrl = field['help_url'];
|
||||
if (!helpUrl) {
|
||||
|
@ -155,21 +157,9 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
|||
return StringBuilderService.buildUrl(helpUrl, config);
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function(counter) {
|
||||
if (counter) {
|
||||
$scope.clearCounter++;
|
||||
$scope.status = '';
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.unauthorizedEmail = false;
|
||||
|
||||
$timeout(function() {
|
||||
if ($scope.defaultData && $scope.defaultData['currentEvent']) {
|
||||
$scope.setEvent($scope.defaultData['currentEvent']);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
$('#createNotificationModal').modal({});
|
||||
$scope.$watch('defaultData', function(counter) {
|
||||
if ($scope.defaultData && $scope.defaultData['currentEvent']) {
|
||||
$scope.setEvent($scope.defaultData['currentEvent']);
|
||||
}
|
||||
});
|
||||
}
|
39
static/js/directives/ui/regex-editor.js
Normal file
39
static/js/directives/ui/regex-editor.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* An element which displays an edit box for regular expressions.
|
||||
*/
|
||||
angular.module('quay').directive('regexEditor', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/regex-editor.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'placeholder': '@placeholder',
|
||||
'optional': '=optional',
|
||||
'binding': '=binding'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
angular.module('quay').directive('requireValidRegex', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
function validator(value) {
|
||||
try {
|
||||
new RegExp(value)
|
||||
ctrl.$setValidity('regex', true);
|
||||
} catch (e) {
|
||||
ctrl.$setValidity('regex', false);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
ctrl.$parsers.push(validator);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -53,14 +53,6 @@ angular.module('quay').directive('repositoryEventsTable', function () {
|
|||
|
||||
loadNotifications();
|
||||
|
||||
$scope.handleNotificationCreated = function(notification) {
|
||||
$scope.notifications.push(notification);
|
||||
};
|
||||
|
||||
$scope.askCreateNotification = function() {
|
||||
$scope.showNewNotificationCounter++;
|
||||
};
|
||||
|
||||
$scope.findEnumValue = function(values, index) {
|
||||
var found = null;
|
||||
Object.keys(values).forEach(function(key) {
|
||||
|
|
33
static/js/pages/create-repository-notification.js
Normal file
33
static/js/pages/create-repository-notification.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
(function() {
|
||||
/**
|
||||
* Create repository notification page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('create-repository-notification', 'create-repository-notification.html', CreateRepoNotificationCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Create Repo Notification: {{ namespace }}/{{ name }}',
|
||||
'description': 'Create repository notification for repository {{ namespace }}/{{ name }}'
|
||||
})
|
||||
}]);
|
||||
|
||||
function CreateRepoNotificationCtrl($scope, $routeParams, $location, ApiService) {
|
||||
$scope.namespace = $routeParams.namespace;
|
||||
$scope.name = $routeParams.name;
|
||||
|
||||
var loadRepository = function() {
|
||||
var params = {
|
||||
'repository': $scope.namespace + '/' + $scope.name
|
||||
};
|
||||
|
||||
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repository = repo;
|
||||
});
|
||||
};
|
||||
|
||||
loadRepository();
|
||||
|
||||
$scope.notificationCreated = function() {
|
||||
$location.url('repository/' + $scope.namespace + '/' + $scope.name + '?tab=settings');
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -20,22 +20,66 @@ function(Config, Features, VulnerabilityService) {
|
|||
{
|
||||
'id': 'build_queued',
|
||||
'title': 'Dockerfile Build Queued',
|
||||
'icon': 'fa-tasks'
|
||||
'icon': 'fa-tasks',
|
||||
'fields': [
|
||||
{
|
||||
'name': 'ref-regex',
|
||||
'type': 'regex',
|
||||
'title': 'matching ref(s)',
|
||||
'help_text': 'An optional regular expression for matching the git branch or tag ' +
|
||||
'git ref. If left blank, the notification will fire for all builds.',
|
||||
'optional': true,
|
||||
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': 'build_start',
|
||||
'title': 'Dockerfile Build Started',
|
||||
'icon': 'fa-circle-o-notch'
|
||||
'icon': 'fa-circle-o-notch',
|
||||
'fields': [
|
||||
{
|
||||
'name': 'ref-regex',
|
||||
'type': 'regex',
|
||||
'title': 'matching ref(s)',
|
||||
'help_text': 'An optional regular expression for matching the git branch or tag ' +
|
||||
'git ref. If left blank, the notification will fire for all builds.',
|
||||
'optional': true,
|
||||
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': 'build_success',
|
||||
'title': 'Dockerfile Build Successfully Completed',
|
||||
'icon': 'fa-check-circle-o'
|
||||
'icon': 'fa-check-circle-o',
|
||||
'fields': [
|
||||
{
|
||||
'name': 'ref-regex',
|
||||
'type': 'regex',
|
||||
'title': 'matching ref(s)',
|
||||
'help_text': 'An optional regular expression for matching the git branch or tag ' +
|
||||
'git ref. If left blank, the notification will fire for all builds.',
|
||||
'optional': true,
|
||||
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': 'build_failure',
|
||||
'title': 'Dockerfile Build Failed',
|
||||
'icon': 'fa-times-circle-o'
|
||||
'icon': 'fa-times-circle-o',
|
||||
'fields': [
|
||||
{
|
||||
'name': 'ref-regex',
|
||||
'type': 'regex',
|
||||
'title': 'matching ref(s)',
|
||||
'help_text': 'An optional regular expression for matching the git branch or tag ' +
|
||||
'git ref. If left blank, the notification will fire for all builds.',
|
||||
'optional': true,
|
||||
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
for (var i = 0; i < buildEvents.length; ++i) {
|
||||
|
@ -52,8 +96,12 @@ function(Config, Features, VulnerabilityService) {
|
|||
{
|
||||
'name': 'level',
|
||||
'type': 'enum',
|
||||
'title': 'Minimum Priority Level',
|
||||
'title': 'minimum severity level',
|
||||
'values': VulnerabilityService.LEVELS,
|
||||
'help_text': 'A vulnerability must have a severity of the chosen level (or higher) ' +
|
||||
'for this notification to fire. Defcon 1 is a special severity level ' +
|
||||
'manually tagged by the ' + Config.REGISTRY_TITLE_SHORT + ' team for ' +
|
||||
'above-critical issues',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -68,7 +116,8 @@ function(Config, Features, VulnerabilityService) {
|
|||
{
|
||||
'name': 'target',
|
||||
'type': 'entity',
|
||||
'title': 'Recipient'
|
||||
'title': 'Recipient',
|
||||
'help_text': 'The ' + Config.REGISTRY_TITLE_SHORT + ' user to notify'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -117,11 +166,11 @@ function(Config, Features, VulnerabilityService) {
|
|||
'fields': [
|
||||
{
|
||||
'name': 'room_id',
|
||||
'type': 'regex',
|
||||
'type': 'pattern',
|
||||
'title': 'Room ID #',
|
||||
'regex': '^[0-9]+$',
|
||||
'pattern': '^[0-9]+$',
|
||||
'help_url': 'https://hipchat.com/admin/rooms',
|
||||
'regex_fail_message': 'We require the HipChat room <b>number</b>, not name.'
|
||||
'pattern_fail_message': 'We require the HipChat room <b>number</b>, not name.'
|
||||
},
|
||||
{
|
||||
'name': 'notification_token',
|
||||
|
@ -138,9 +187,9 @@ function(Config, Features, VulnerabilityService) {
|
|||
'fields': [
|
||||
{
|
||||
'name': 'url',
|
||||
'type': 'regex',
|
||||
'type': 'pattern',
|
||||
'title': 'Webhook URL',
|
||||
'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
|
||||
'pattern': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
|
||||
'help_url': 'https://slack.com/services/new/incoming-webhook',
|
||||
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
|
||||
}
|
||||
|
|
21
static/partials/create-repository-notification.html
Normal file
21
static/partials/create-repository-notification.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div class="resource-view"
|
||||
resource="repositoryResource"
|
||||
error-message="'Repository not found'">
|
||||
<div class="page-content">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link">
|
||||
<a class="back-link" href="/repository/{{ namespace }}/{{ name }}">
|
||||
{{ namespace }} / {{ name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="cor-title-content">
|
||||
<i class="fa fa-bell" style="margin-right: 10px"></i>
|
||||
Create repository notification
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel">
|
||||
<div class="create-external-notification" repository="repository" notification-created="notificationCreated()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
135
test/test_notifications.py
Normal file
135
test/test_notifications.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
import unittest
|
||||
|
||||
from endpoints.notificationevent import BuildSuccessEvent
|
||||
from util.morecollections import AttrDict
|
||||
|
||||
class TestShouldPerform(unittest.TestCase):
|
||||
def test_build_nofilter(self):
|
||||
notification_data = AttrDict({
|
||||
'event_config_json': '{}',
|
||||
})
|
||||
|
||||
# No build data at all.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data))
|
||||
|
||||
# With trigger metadata but no ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/somebranch',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
|
||||
def test_build_emptyfilter(self):
|
||||
notification_data = AttrDict({
|
||||
'event_config_json': '{"ref-regex": ""}',
|
||||
})
|
||||
|
||||
# No build data at all.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data))
|
||||
|
||||
# With trigger metadata but no ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/somebranch',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
|
||||
def test_build_invalidfilter(self):
|
||||
notification_data = AttrDict({
|
||||
'event_config_json': '{"ref-regex": "]["}',
|
||||
})
|
||||
|
||||
# No build data at all.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
|
||||
|
||||
# With trigger metadata but no ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/somebranch',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
|
||||
def test_build_withfilter(self):
|
||||
notification_data = AttrDict({
|
||||
'event_config_json': '{"ref-regex": "refs/heads/master"}',
|
||||
})
|
||||
|
||||
# No build data at all.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
|
||||
|
||||
# With trigger metadata but no ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a not-matching ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/somebranch',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a matching ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/master',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
|
||||
def test_build_withwildcardfilter(self):
|
||||
notification_data = AttrDict({
|
||||
'event_config_json': '{"ref-regex": "refs/heads/.+"}',
|
||||
})
|
||||
|
||||
# No build data at all.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
|
||||
|
||||
# With trigger metadata but no ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a not-matching ref.
|
||||
self.assertFalse(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/tags/sometag',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and a matching ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/master',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
# With trigger metadata and another matching ref.
|
||||
self.assertTrue(BuildSuccessEvent().should_perform({
|
||||
'trigger_metadata': {
|
||||
'ref': 'refs/heads/somebranch',
|
||||
},
|
||||
}, notification_data))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Reference in a new issue