Merge pull request #4291 from jladdjr/templated_messages

Templated notifications

Reviewed-by: Jim Ladd
             https://github.com/jladdjr
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-08-27 16:29:21 +00:00
committed by GitHub
37 changed files with 1252 additions and 96 deletions

View File

@@ -485,6 +485,8 @@
}
.CodeMirror {
min-height: initial !important;
max-height: initial !important;
border-radius: 5px;
font-style: normal;
color: @field-input-text;

View File

@@ -42,6 +42,7 @@ import toolbar from '~components/list/list-toolbar.directive';
import topNavItem from '~components/layout/top-nav-item.directive';
import truncate from '~components/truncate/truncate.directive';
import atCodeMirror from '~components/code-mirror';
import atSyntaxHighlight from '~components/syntax-highlight';
import card from '~components/cards/card.directive';
import cardGroup from '~components/cards/group.directive';
import atSwitch from '~components/switch/switch.directive';
@@ -54,7 +55,8 @@ const MODULE_NAME = 'at.lib.components';
angular
.module(MODULE_NAME, [
atLibServices,
atCodeMirror
atCodeMirror,
atSyntaxHighlight,
])
.directive('atActionGroup', actionGroup)
.directive('atActionButton', actionButton)

View File

@@ -0,0 +1,8 @@
import syntaxHighlight from './syntax-highlight.directive';
const MODULE_NAME = 'at.syntax.highlight';
angular.module(MODULE_NAME, [])
.directive('atSyntaxHighlight', syntaxHighlight);
export default MODULE_NAME;

View File

@@ -0,0 +1,98 @@
const templateUrl = require('~components/syntax-highlight/syntax-highlight.partial.html');
function atSyntaxHighlightController ($scope, AngularCodeMirror) {
const vm = this;
const varName = `${$scope.name}_codemirror`;
function init () {
if ($scope.disabled === 'true') {
$scope.disabled = true;
} else if ($scope.disabled === 'false') {
$scope.disabled = false;
}
$scope.value = $scope.value || $scope.default;
initCodeMirror();
$scope.$watch(varName, () => {
$scope.value = $scope[varName];
if ($scope.oneLine && $scope.value && $scope.value.includes('\n')) {
$scope.hasNewlineError = true;
} else {
$scope.hasNewlineError = false;
}
});
}
function initCodeMirror () {
$scope.varName = varName;
$scope[varName] = $scope.value;
const codeMirror = AngularCodeMirror(!!$scope.disabled);
codeMirror.addModes({
jinja2: {
mode: $scope.mode,
matchBrackets: true,
autoCloseBrackets: true,
styleActiveLine: true,
lineNumbers: true,
gutters: ['CodeMirror-lint-markers'],
lint: true,
scrollbarStyle: null,
}
});
if (document.querySelector(`.ng-hide #${$scope.name}_codemirror`)) {
return;
}
codeMirror.showTextArea({
scope: $scope,
model: varName,
element: `${$scope.name}_codemirror`,
lineNumbers: true,
mode: $scope.mode,
});
}
vm.name = $scope.name;
vm.rows = $scope.rows || 6;
if ($scope.init) {
$scope.init = init;
}
angular.element(document).ready(() => {
init();
});
$scope.$on('reset-code-mirror', () => {
setImmediate(initCodeMirror);
});
}
atSyntaxHighlightController.$inject = [
'$scope',
'AngularCodeMirror'
];
function atCodeMirrorTextarea () {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl,
controller: atSyntaxHighlightController,
controllerAs: 'vm',
scope: {
disabled: '@',
label: '@',
labelClass: '@',
tooltip: '@',
tooltipPlacement: '@',
value: '=',
name: '@',
init: '=',
default: '@',
rows: '@',
oneLine: '@',
mode: '@',
}
};
}
export default atCodeMirrorTextarea;

View File

@@ -0,0 +1,33 @@
<div>
<div class="atCodeMirror-label">
<div class="atCodeMirror-labelLeftSide">
<span class="atCodeMirror-labelText" ng-class="labelClass">
{{ label || vm.strings.get('code_mirror.label.VARIABLES') }}
</span>
<a
id=""
href=""
aw-pop-over="{{ tooltip || vm.strings.get('code_mirror.tooltip.TOOLTIP') }}"
data-placement="{{ tooltipPlacement || 'top' }}"
data-container="body"
over-title="{{ label || vm.strings.get('code_mirror.label.VARIABLES') }}"
class="help-link"
data-original-title="{{ label || vm.strings.get('code_mirror.label.VARIABLES') }}"
title="{{ label || vm.strings.get('code_mirror.label.VARIABLES') }}"
tabindex="-1"
ng-if="!!tooltip"
>
<i class="fa fa-question-circle"></i>
</a>
</div>
</div>
<textarea
ng-disabled="disabled"
rows="{{ vm.rows }}"
ng-model="codeMirrorValue"
name="{{ vm.name }}_codemirror"
class="form-control Form-textArea"
id="{{ vm.name }}_codemirror">
</textarea>
<div class="error" ng-show="hasNewlineError">New lines are not supported in this field</div>
</div>

View File

@@ -7,21 +7,24 @@
export default ['Rest', 'Wait', 'NotificationsFormObject',
'ProcessErrors', 'GetBasePath', 'Alert',
'GenerateForm', '$scope', '$state', 'CreateSelect2', 'GetChoices',
'NotificationsTypeChange', 'ParseTypeChange', 'i18n',
'NotificationsTypeChange', 'ParseTypeChange', 'i18n', 'MessageUtils', '$filter',
function(
Rest, Wait, NotificationsFormObject,
ProcessErrors, GetBasePath, Alert,
GenerateForm, $scope, $state, CreateSelect2, GetChoices,
NotificationsTypeChange, ParseTypeChange, i18n
NotificationsTypeChange, ParseTypeChange, i18n,
MessageUtils, $filter
) {
var generator = GenerateForm,
form = NotificationsFormObject,
url = GetBasePath('notification_templates');
url = GetBasePath('notification_templates'),
defaultMessages = {};
init();
function init() {
$scope.customize_messages = false;
Rest.setUrl(GetBasePath('notification_templates'));
Rest.options()
.then(({data}) => {
@@ -29,6 +32,8 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a notification template.', 'alert-info');
}
defaultMessages = data.actions.GET.messages;
MessageUtils.setMessagesOnScope($scope, null, defaultMessages);
});
// apply form definition's default field values
GenerateForm.applyDefaults(form, $scope);
@@ -153,6 +158,29 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
});
};
$scope.$watch('customize_messages', (value) => {
if (value) {
$scope.$broadcast('reset-code-mirror', {
customize_messages: $scope.customize_messages,
});
}
});
$scope.toggleForm = function(key) {
$scope[key] = !$scope[key];
};
$scope.$watch('notification_type', (newValue, oldValue = {}) => {
if (newValue) {
MessageUtils.updateDefaultsOnScope(
$scope,
defaultMessages[oldValue.value],
defaultMessages[newValue.value]
);
$scope.$broadcast('reset-code-mirror', {
customize_messages: $scope.customize_messages,
});
}
});
$scope.emailOptionsChange = function () {
if ($scope.email_options === 'use_ssl') {
if ($scope.use_ssl) {
@@ -186,6 +214,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
"name": $scope.name,
"description": $scope.description,
"organization": $scope.organization,
"messages": MessageUtils.getMessagesObj($scope, defaultMessages),
"notification_type": v,
"notification_configuration": {}
};
@@ -238,10 +267,14 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
$state.go('notifications', {}, { reload: true });
Wait('stop');
})
.catch(({data, status}) => {
.catch(({ data, status }) => {
let description = 'POST returned status: ' + status;
if (data && data.messages && data.messages.length > 0) {
description = _.uniq(data.messages).join(', ');
}
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to add new notifier. POST returned status: ' + status
msg: $filter('sanitize')('Failed to add new notifier. ' + description + '.')
});
});
};

View File

@@ -10,19 +10,22 @@ export default ['Rest', 'Wait',
'notification_template',
'$scope', '$state', 'GetChoices', 'CreateSelect2', 'Empty',
'NotificationsTypeChange', 'ParseTypeChange', 'i18n',
'MessageUtils', '$filter',
function(
Rest, Wait,
NotificationsFormObject, ProcessErrors, GetBasePath,
GenerateForm,
notification_template,
$scope, $state, GetChoices, CreateSelect2, Empty,
NotificationsTypeChange, ParseTypeChange, i18n
NotificationsTypeChange, ParseTypeChange, i18n,
MessageUtils, $filter
) {
var generator = GenerateForm,
id = notification_template.id,
form = NotificationsFormObject,
master = {},
url = GetBasePath('notification_templates');
url = GetBasePath('notification_templates'),
defaultMessages = {};
init();
@@ -35,6 +38,12 @@ export default ['Rest', 'Wait',
}
});
Rest.setUrl(GetBasePath('notification_templates'));
Rest.options()
.then(({data}) => {
defaultMessages = data.actions.GET.messages;
});
GetChoices({
scope: $scope,
url: url,
@@ -165,6 +174,9 @@ export default ['Rest', 'Wait',
field_id: 'notification_template_headers',
readOnly: !$scope.notification_template.summary_fields.user_capabilities.edit
});
MessageUtils.setMessagesOnScope($scope, data.messages, defaultMessages);
Wait('stop');
})
.catch(({data, status}) => {
@@ -175,8 +187,6 @@ export default ['Rest', 'Wait',
});
});
$scope.$watch('headers', function validate_headers(str) {
try {
let headers = JSON.parse(str);
@@ -237,6 +247,29 @@ export default ['Rest', 'Wait',
});
};
$scope.$watch('customize_messages', (value) => {
if (value) {
$scope.$broadcast('reset-code-mirror', {
customize_messages: $scope.customize_messages,
});
}
});
$scope.toggleForm = function(key) {
$scope[key] = !$scope[key];
};
$scope.$watch('notification_type', (newValue, oldValue = {}) => {
if (newValue) {
MessageUtils.updateDefaultsOnScope(
$scope,
defaultMessages[oldValue.value],
defaultMessages[newValue.value]
);
$scope.$broadcast('reset-code-mirror', {
customize_messages: $scope.customize_messages,
});
}
});
$scope.emailOptionsChange = function () {
if ($scope.email_options === 'use_ssl') {
if ($scope.use_ssl) {
@@ -269,6 +302,7 @@ export default ['Rest', 'Wait',
"name": $scope.name,
"description": $scope.description,
"organization": $scope.organization,
"messages": MessageUtils.getMessagesObj($scope, defaultMessages),
"notification_type": v,
"notification_configuration": {}
};
@@ -316,10 +350,14 @@ export default ['Rest', 'Wait',
$state.go('notifications', {}, { reload: true });
Wait('stop');
})
.catch(({data, status}) => {
.catch(({ data, status }) => {
let description = 'PUT returned status: ' + status;
if (data && data.messages && data.messages.length > 0) {
description = _.uniq(data.messages).join(', ');
}
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to add new notification template. POST returned status: ' + status
msg: $filter('sanitize')('Failed to update notifier. ' + description + '.')
});
});
};

View File

@@ -15,6 +15,7 @@ import notificationsList from './notifications.list';
import toggleNotification from './shared/toggle-notification.factory';
import notificationsListInit from './shared/notification-list-init.factory';
import typeChange from './shared/type-change.service';
import messageUtils from './shared/message-utils.service';
import { N_ } from '../i18n';
export default
@@ -29,6 +30,7 @@ angular.module('notifications', [
.factory('ToggleNotification', toggleNotification)
.factory('NotificationsListInit', notificationsListInit)
.service('NotificationsTypeChange', typeChange)
.service('MessageUtils', messageUtils)
.config(['$stateProvider', 'stateDefinitionsProvider',
function($stateProvider, stateDefinitionsProvider) {
let stateDefinitions = stateDefinitionsProvider.$get();

View File

@@ -428,7 +428,7 @@ export default ['i18n', function(i18n) {
dataTitle: i18n._('HTTP Method'),
type: 'select',
ngOptions: 'choice.id as choice.name for choice in httpMethodChoices',
default: 'post',
default: 'POST',
awPopOver: i18n._('Specify an HTTP method for the webhook. Acceptable choices are: POST or PUT'),
awRequiredWhen: {
reqExpression: "webhook_required",
@@ -581,7 +581,96 @@ export default ['i18n', function(i18n) {
ngShow: "notification_type.value == 'slack' ",
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
awPopOver: i18n._('Specify a notification color. Acceptable colors are hex color code (example: #3af or #789abc) .')
}
},
customize_messages: {
label: i18n._('Customize messages…'),
type: 'toggleSwitch',
toggleSource: 'customize_messages',
class: 'Form-formGroup--fullWidth',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
custom_message_description: {
type: 'alertblock',
ngShow: "customize_messages",
alertTxt: i18n._('Use custom messages to change the content of notifications ' +
'sent when a job starts, succeeds, or fails. Use curly braces to access ' +
'information about the job: <code ng-non-bindable>{{ job_friendly_name }}</code>, ' +
'<code ng-non-bindable>{{ url }}</code>, or attributes of the job such as ' +
'<code ng-non-bindable>{{ job.status }}</code>. You may apply a number of possible ' +
'variables in the message. Refer to the ' +
'<a href="https://docs.ansible.com/ansible-tower/latest/html/userguide/notifications.html#create-a-notification-template" ' +
'target="_blank">Ansible Tower documentation</a> for more details.'),
closeable: false
},
started_message: {
label: i18n._('Start Message'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && notification_type.value != 'webhook'",
rows: 2,
oneLine: 'true',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
started_body: {
label: i18n._('Start Message Body'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && " +
"(notification_type.value == 'email' " +
"|| notification_type.value == 'pagerduty' " +
"|| notification_type.value == 'webhook')",
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
success_message: {
label: i18n._('Success Message'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && notification_type.value != 'webhook'",
rows: 2,
oneLine: 'true',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
success_body: {
label: i18n._('Success Message Body'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && " +
"(notification_type.value == 'email' " +
"|| notification_type.value == 'pagerduty' " +
"|| notification_type.value == 'webhook')",
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
error_message: {
label: i18n._('Error Message'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && notification_type.value != 'webhook'",
rows: 2,
oneLine: 'true',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
error_body: {
label: i18n._('Error Message Body'),
class: 'Form-formGroup--fullWidth',
type: 'syntax_highlight',
mode: 'jinja2',
default: '',
ngShow: "customize_messages && " +
"(notification_type.value == 'email' " +
"|| notification_type.value == 'pagerduty' " +
"|| notification_type.value == 'webhook')",
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)',
},
},
buttons: { //for now always generates <button> tags

View File

@@ -0,0 +1,115 @@
const emptyDefaults = {
started: {
message: '',
body: '',
},
success: {
message: '',
body: '',
},
error: {
message: '',
body: '',
},
};
export default [function() {
return {
getMessagesObj: function ($scope, defaultMessages) {
if (!$scope.customize_messages) {
return null;
}
const defaults = defaultMessages[$scope.notification_type.value] || {};
return {
started: {
message: $scope.started_message === defaults.started.message ?
null : $scope.started_message,
body: $scope.started_body === defaults.started.body ?
null : $scope.started_body,
},
success: {
message: $scope.success_message === defaults.success.message ?
null : $scope.success_message,
body: $scope.success_body === defaults.success.body ?
null : $scope.success_body,
},
error: {
message: $scope.error_message === defaults.error.message ?
null : $scope.error_message,
body: $scope.error_body === defaults.error.body ?
null : $scope.error_body,
}
};
},
setMessagesOnScope: function ($scope, messages, defaultMessages) {
let defaults;
if ($scope.notification_type) {
defaults = defaultMessages[$scope.notification_type.value] || emptyDefaults;
} else {
defaults = emptyDefaults;
}
$scope.started_message = defaults.started.message;
$scope.started_body = defaults.started.body;
$scope.success_message = defaults.success.message;
$scope.success_body = defaults.success.body;
$scope.error_message = defaults.error.message;
$scope.error_body = defaults.error.body;
if (!messages) {
return;
}
let isCustomized = false;
if (messages.started.message) {
isCustomized = true;
$scope.started_message = messages.started.message;
}
if (messages.started.body) {
isCustomized = true;
$scope.started_body = messages.started.body;
}
if (messages.success.message) {
isCustomized = true;
$scope.success_message = messages.success.message;
}
if (messages.success.body) {
isCustomized = true;
$scope.success_body = messages.success.body;
}
if (messages.error.message) {
isCustomized = true;
$scope.error_message = messages.error.message;
}
if (messages.error.body) {
isCustomized = true;
$scope.error_body = messages.error.body;
}
$scope.customize_messages = isCustomized;
},
updateDefaultsOnScope: function(
$scope,
oldDefaults = emptyDefaults,
newDefaults = emptyDefaults
) {
if ($scope.started_message === oldDefaults.started.message) {
$scope.started_message = newDefaults.started.message;
}
if ($scope.started_body === oldDefaults.started.body) {
$scope.started_body = newDefaults.started.body;
}
if ($scope.success_message === oldDefaults.success.message) {
$scope.success_message = newDefaults.success.message;
}
if ($scope.success_body === oldDefaults.success.body) {
$scope.success_body = newDefaults.success.body;
}
if ($scope.error_message === oldDefaults.error.message) {
$scope.error_message = newDefaults.error.message;
}
if ($scope.error_body === oldDefaults.error.body) {
$scope.error_body = newDefaults.error.body;
}
}
};
}];

View File

@@ -697,7 +697,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += `<div id='${form.name}_${fld}_group' class='form-group Form-formGroup `;
html += (field.disabled) ? `Form-formGroup--disabled ` : ``;
html += (field.type === "checkbox") ? "Form-formGroup--checkbox" : "";
html += (field.type === "checkbox") ? "Form-formGroup--checkbox " : "";
html += (field['class']) ? (field['class']) : "";
html += "'";
html += (field.ngShow) ? this.attr(field, 'ngShow') : "";
@@ -1359,6 +1359,22 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += '></at-code-mirror>';
}
if (field.type === 'syntax_highlight') {
html += '<at-syntax-highlight ';
html += `id="${form.name}_${fld}" `;
html += `class="${field.class}" `;
html += `label="${field.label}" `;
html += `tooltip="${field.awPopOver || ''}" `;
html += `name="${fld}" `;
html += `value="${fld}" `;
html += `default="${field.default || ''}" `;
html += `rows="${field.rows || 6}" `;
html += `one-line="${field.oneLine || ''}"`;
html += `mode="${field.mode}" `;
html += `ng-disabled="${field.ngDisabled}" `;
html += '></at-syntax-highlight>';
}
if (field.type === 'custom') {
let labelOptions = {};

View File

@@ -1,6 +1,7 @@
import 'codemirror/lib/codemirror.js';
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/yaml/yaml.js';
import 'codemirror/mode/jinja2/jinja2.js';
import 'codemirror/addon/lint/lint.js';
import 'angular-codemirror/lib/yaml-lint.js';
import 'codemirror/addon/edit/closebrackets.js';

View File

@@ -72,3 +72,4 @@ require('ng-toast');
require('lr-infinite-scroll');
require('codemirror/mode/yaml/yaml');
require('codemirror/mode/javascript/javascript');
require('codemirror/mode/jinja2/jinja2');