From 72ae8f46c9af5b47ae6919a202ac96a3eccb0ca1 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Mar 2016 16:18:18 -0700 Subject: [PATCH] Adding notifications into the UI --- awx/ui/client/lib/ngToast/.bower.json | 59 +++ awx/ui/client/lib/ngToast/README.md | 119 ++++++ awx/ui/client/lib/ngToast/bower.json | 49 +++ .../lib/ngToast/dist/ngToast-animations.css | 107 ++++++ .../ngToast/dist/ngToast-animations.min.css | 7 + awx/ui/client/lib/ngToast/dist/ngToast.css | 60 +++ awx/ui/client/lib/ngToast/dist/ngToast.js | 284 ++++++++++++++ .../client/lib/ngToast/dist/ngToast.min.css | 7 + awx/ui/client/lib/ngToast/dist/ngToast.min.js | 7 + awx/ui/client/src/app.js | 12 +- awx/ui/client/src/controllers/Projects.js | 29 +- awx/ui/client/src/forms/Credentials.js | 2 +- awx/ui/client/src/forms/JobTemplates.js | 18 +- awx/ui/client/src/forms/Organizations.js | 25 +- awx/ui/client/src/forms/Projects.js | 27 +- awx/ui/client/src/helpers/JobTemplates.js | 2 +- .../edit/job-templates-edit.controller.js | 29 +- .../src/notifications/add/add.controller.js | 130 +++++-- .../client/src/notifications/add/add.route.js | 2 +- .../src/notifications/edit/edit.controller.js | 196 +++++++--- .../src/notifications/edit/edit.route.js | 34 +- .../src/notifications/list/list.controller.js | 83 ----- awx/ui/client/src/notifications/main.js | 20 +- .../list.controller.js | 212 +++++++++++ .../list.partial.html | 0 .../list.route.js | 4 +- .../main.js | 4 +- .../notificationTemplates.form.js | 351 ++++++++++++++++++ .../notificationTemplates.list.js | 88 +++++ .../notifications/notifications.block.less | 7 + .../src/notifications/notifications.form.js | 47 --- .../src/notifications/notifications.list.js | 65 ++-- .../shared/notification-list-init.factory.js | 57 +++ .../shared/toggle-notification.factory.js | 90 +++++ .../shared/type-change.service.js | 73 ++++ .../edit/organizations-edit.controller.js | 32 +- awx/ui/client/src/shared/directives.js | 2 +- awx/ui/client/src/shared/form-generator.js | 17 +- awx/ui/client/src/shared/generator-helpers.js | 11 +- .../list-generator/list-generator.factory.js | 2 +- awx/ui/templates/ui/index.html | 4 +- 41 files changed, 2063 insertions(+), 311 deletions(-) create mode 100644 awx/ui/client/lib/ngToast/.bower.json create mode 100644 awx/ui/client/lib/ngToast/README.md create mode 100644 awx/ui/client/lib/ngToast/bower.json create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast-animations.css create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast.css create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast.js create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast.min.css create mode 100644 awx/ui/client/lib/ngToast/dist/ngToast.min.js delete mode 100644 awx/ui/client/src/notifications/list/list.controller.js create mode 100644 awx/ui/client/src/notifications/notification-templates-list/list.controller.js rename awx/ui/client/src/notifications/{list => notification-templates-list}/list.partial.html (100%) rename awx/ui/client/src/notifications/{list => notification-templates-list}/list.route.js (77%) rename awx/ui/client/src/notifications/{list => notification-templates-list}/main.js (75%) create mode 100644 awx/ui/client/src/notifications/notificationTemplates.form.js create mode 100644 awx/ui/client/src/notifications/notificationTemplates.list.js create mode 100644 awx/ui/client/src/notifications/notifications.block.less delete mode 100644 awx/ui/client/src/notifications/notifications.form.js create mode 100644 awx/ui/client/src/notifications/shared/notification-list-init.factory.js create mode 100644 awx/ui/client/src/notifications/shared/toggle-notification.factory.js create mode 100644 awx/ui/client/src/notifications/shared/type-change.service.js diff --git a/awx/ui/client/lib/ngToast/.bower.json b/awx/ui/client/lib/ngToast/.bower.json new file mode 100644 index 0000000000..446800a71d --- /dev/null +++ b/awx/ui/client/lib/ngToast/.bower.json @@ -0,0 +1,59 @@ +{ + "name": "ngToast", + "version": "2.0.0", + "description": "Angular provider for toast notifications", + "main": [ + "dist/ngToast.js", + "dist/ngToast.css" + ], + "keywords": [ + "angular", + "toast", + "message", + "notification", + "toastr" + ], + "repository": { + "type": "git", + "url": "git://github.com/tameraydin/ngToast.git" + }, + "homepage": "http://tameraydin.github.io/ngToast", + "authors": [ + "Tamer Aydin (http://tamerayd.in)", + "Levi Thomason (http://www.levithomason.com)" + ], + "license": "MIT", + "dependencies": { + "angular": ">=1.2.15 <1.6", + "angular-sanitize": ">=1.2.15 <1.6" + }, + "devDependencies": { + "angular-animate": ">=1.2.17 <1.6", + "bootstrap": "~3.3.2", + "Faker": "~2.1.2" + }, + "ignore": [ + "**/.*", + "node_modules", + "test", + "src", + ".editorconfig", + ".gitignore", + ".gitattributes", + ".jshintrc", + ".travis.yml", + "Gruntfile.js", + "package.json", + "index.html" + ], + "_release": "2.0.0", + "_resolution": { + "type": "version", + "tag": "2.0.0", + "commit": "8a1951c54a956c33964c99b338f3a4830e652689" + }, + "_source": "git://github.com/tameraydin/ngToast.git", + "_target": "~2.0.0", + "_originalSource": "ngtoast", + "_direct": true +} \ No newline at end of file diff --git a/awx/ui/client/lib/ngToast/README.md b/awx/ui/client/lib/ngToast/README.md new file mode 100644 index 0000000000..186930a86d --- /dev/null +++ b/awx/ui/client/lib/ngToast/README.md @@ -0,0 +1,119 @@ +ngToast [![Code Climate](http://img.shields.io/codeclimate/github/tameraydin/ngToast.svg?style=flat-square)](https://codeclimate.com/github/tameraydin/ngToast/dist/ngToast.js) [![Build Status](http://img.shields.io/travis/tameraydin/ngToast/master.svg?style=flat-square)](https://travis-ci.org/tameraydin/ngToast) +======= + +ngToast is a simple Angular provider for toast notifications. + +**[Demo](http://tameraydin.github.io/ngToast)** + +## Usage + +1. Install via [Bower](http://bower.io/) or [NPM](http://www.npmjs.org): + ```bash + bower install ngtoast --production + # or + npm install ng-toast --production + ``` + or manually [download](https://github.com/tameraydin/ngToast/archive/master.zip). + +2. Include ngToast source files and dependencies ([ngSanitize](http://docs.angularjs.org/api/ngSanitize), [Bootstrap CSS](http://getbootstrap.com/)): + ```html + + + + + + ``` + *Note: only the [Alerts](http://getbootstrap.com/components/#alerts) component is used as style base, so you don't have to include complete CSS* + +3. Include ngToast as a dependency in your application module: + ```javascript + var app = angular.module('myApp', ['ngToast']); + ``` + +4. Place `toast` element into your HTML: + ```html + + + ... + + ``` + +5. Inject ngToast provider in your controller: + ```javascript + app.controller('myCtrl', function(ngToast) { + ngToast.create('a toast message...'); + }); + // for more info: http://tameraydin.github.io/ngToast/#api + ``` + +## Animations +ngToast comes with optional animations. In order to enable animations in ngToast, you need to include [ngAnimate](http://docs.angularjs.org/api/ngAnimate) module into your app: + +```html + +``` + +**Built-in** + 1. Include the ngToast animation stylesheet: + + ```html + + ``` + + 2. Set the `animation` option. + ```javascript + app.config(['ngToastProvider', function(ngToastProvider) { + ngToastProvider.configure({ + animation: 'slide' // or 'fade' + }); + }]); + ``` + Built-in ngToast animations include `slide` & `fade`. + +**Custom** + + See the [plunker](http://plnkr.co/edit/wglAvsCuTLLykLNqVGwU) using [animate.css](http://daneden.github.io/animate.css/). + + 1. Using the `additionalClasses` option and [ngAnimate](http://docs.angularjs.org/api/ngAnimate) you can easily add your own animations or wire up 3rd party css animations. + ```javascript + app.config(['ngToastProvider', function(ngToastProvider) { + ngToastProvider.configure({ + additionalClasses: 'my-animation' + }); + }]); + ``` + + 2. Then in your CSS (example using animate.css): + ```css + /* Add any vendor prefixes you need */ + .my-animation.ng-enter { + animation: flipInY 1s; + } + + .my-animation.ng-leave { + animation: flipOutY 1s; + } + ``` + +## Settings & API + +Please find at the [project website](http://tameraydin.github.io/ngToast/#api). + +## Development + +* Clone the repo or [download](https://github.com/tameraydin/ngToast/archive/master.zip) +* Install dependencies: ``npm install && bower install`` +* Run ``grunt watch``, play on **/src** +* Build: ``grunt`` + +## License + +MIT [http://tameraydin.mit-license.org/](http://tameraydin.mit-license.org/) + +## Maintainers + +- [Tamer Aydin](http://tamerayd.in) +- [Levi Thomason](http://www.levithomason.com) + +## TODO +- Add more unit & e2e tests diff --git a/awx/ui/client/lib/ngToast/bower.json b/awx/ui/client/lib/ngToast/bower.json new file mode 100644 index 0000000000..4e116404ba --- /dev/null +++ b/awx/ui/client/lib/ngToast/bower.json @@ -0,0 +1,49 @@ +{ + "name": "ngToast", + "version": "2.0.0", + "description": "Angular provider for toast notifications", + "main": [ + "dist/ngToast.js", + "dist/ngToast.css" + ], + "keywords": [ + "angular", + "toast", + "message", + "notification", + "toastr" + ], + "repository": { + "type": "git", + "url": "git://github.com/tameraydin/ngToast.git" + }, + "homepage": "http://tameraydin.github.io/ngToast", + "authors": [ + "Tamer Aydin (http://tamerayd.in)", + "Levi Thomason (http://www.levithomason.com)" + ], + "license": "MIT", + "dependencies": { + "angular": ">=1.2.15 <1.6", + "angular-sanitize": ">=1.2.15 <1.6" + }, + "devDependencies": { + "angular-animate": ">=1.2.17 <1.6", + "bootstrap": "~3.3.2", + "Faker": "~2.1.2" + }, + "ignore": [ + "**/.*", + "node_modules", + "test", + "src", + ".editorconfig", + ".gitignore", + ".gitattributes", + ".jshintrc", + ".travis.yml", + "Gruntfile.js", + "package.json", + "index.html" + ] +} diff --git a/awx/ui/client/lib/ngToast/dist/ngToast-animations.css b/awx/ui/client/lib/ngToast/dist/ngToast-animations.css new file mode 100644 index 0000000000..905bd4d3d9 --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast-animations.css @@ -0,0 +1,107 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast--animate-fade .ng-enter, +.ng-toast--animate-fade .ng-leave, +.ng-toast--animate-fade .ng-move { + transition-property: opacity; + transition-duration: 0.3s; + transition-timing-function: ease; } + +.ng-toast--animate-fade .ng-enter { + opacity: 0; } + +.ng-toast--animate-fade .ng-enter.ng-enter-active { + opacity: 1; } + +.ng-toast--animate-fade .ng-leave { + opacity: 1; } + +.ng-toast--animate-fade .ng-leave.ng-leave-active { + opacity: 0; } + +.ng-toast--animate-fade .ng-move { + opacity: 0.5; } + +.ng-toast--animate-fade .ng-move.ng-move-active { + opacity: 1; } + +.ng-toast--animate-slide .ng-enter, +.ng-toast--animate-slide .ng-leave, +.ng-toast--animate-slide .ng-move { + position: relative; + transition-duration: 0.3s; + transition-timing-function: ease; } + +.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message { + position: relative; + transition-property: top, margin-top, opacity; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter { + opacity: 0; + top: -100px; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter.ng-enter-active { + opacity: 1; + top: 0; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave { + opacity: 1; + top: 0; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave.ng-leave-active { + opacity: 0; + margin-top: -72px; } + +.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message { + position: relative; + transition-property: bottom, margin-bottom, opacity; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter { + opacity: 0; + bottom: -100px; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter.ng-enter-active { + opacity: 1; + bottom: 0; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave { + opacity: 1; + bottom: 0; } + .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave.ng-leave-active { + opacity: 0; + margin-bottom: -72px; } + +.ng-toast--animate-slide.ng-toast--right { + transition-property: right, margin-right, opacity; } + .ng-toast--animate-slide.ng-toast--right .ng-enter { + opacity: 0; + right: -200%; + margin-right: 20px; } + .ng-toast--animate-slide.ng-toast--right .ng-enter.ng-enter-active { + opacity: 1; + right: 0; + margin-right: 0; } + .ng-toast--animate-slide.ng-toast--right .ng-leave { + opacity: 1; + right: 0; + margin-right: 0; } + .ng-toast--animate-slide.ng-toast--right .ng-leave.ng-leave-active { + opacity: 0; + right: -200%; + margin-right: 20px; } + +.ng-toast--animate-slide.ng-toast--left { + transition-property: left, margin-left, opacity; } + .ng-toast--animate-slide.ng-toast--left .ng-enter { + opacity: 0; + left: -200%; + margin-left: 20px; } + .ng-toast--animate-slide.ng-toast--left .ng-enter.ng-enter-active { + opacity: 1; + left: 0; + margin-left: 0; } + .ng-toast--animate-slide.ng-toast--left .ng-leave { + opacity: 1; + left: 0; + margin-left: 0; } + .ng-toast--animate-slide.ng-toast--left .ng-leave.ng-leave-active { + opacity: 0; + left: -200%; + margin-left: 20px; } diff --git a/awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css b/awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css new file mode 100644 index 0000000000..607552e896 --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast--animate-fade .ng-enter,.ng-toast--animate-fade .ng-leave,.ng-toast--animate-fade .ng-move{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-fade .ng-enter{opacity:0}.ng-toast--animate-fade .ng-enter.ng-enter-active,.ng-toast--animate-fade .ng-leave{opacity:1}.ng-toast--animate-fade .ng-leave.ng-leave-active{opacity:0}.ng-toast--animate-fade .ng-move{opacity:.5}.ng-toast--animate-fade .ng-move.ng-move-active{opacity:1}.ng-toast--animate-slide .ng-enter,.ng-toast--animate-slide .ng-leave,.ng-toast--animate-slide .ng-move{position:relative;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message{position:relative;transition-property:top,margin-top,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter{opacity:0;top:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave{opacity:1;top:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-top:-72px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message{position:relative;transition-property:bottom,margin-bottom,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter{opacity:0;bottom:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave{opacity:1;bottom:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-bottom:-72px}.ng-toast--animate-slide.ng-toast--right{transition-property:right,margin-right,opacity}.ng-toast--animate-slide.ng-toast--right .ng-enter{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--right .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--right .ng-leave{opacity:1;right:0;margin-right:0}.ng-toast--animate-slide.ng-toast--right .ng-leave.ng-leave-active{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--left{transition-property:left,margin-left,opacity}.ng-toast--animate-slide.ng-toast--left .ng-enter{opacity:0;left:-200%;margin-left:20px}.ng-toast--animate-slide.ng-toast--left .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--left .ng-leave{opacity:1;left:0;margin-left:0}.ng-toast--animate-slide.ng-toast--left .ng-leave.ng-leave-active{opacity:0;left:-200%;margin-left:20px} \ No newline at end of file diff --git a/awx/ui/client/lib/ngToast/dist/ngToast.css b/awx/ui/client/lib/ngToast/dist/ngToast.css new file mode 100644 index 0000000000..790b82e28b --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast.css @@ -0,0 +1,60 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast { + position: fixed; + z-index: 1080; + width: 100%; + height: 0; + margin-top: 20px; + text-align: center; } + .ng-toast.ng-toast--top { + top: 0; + bottom: auto; } + .ng-toast.ng-toast--top .ng-toast__list { + top: 0; + bottom: auto; } + .ng-toast.ng-toast--top.ng-toast--center .ng-toast__list { + position: static; } + .ng-toast.ng-toast--bottom { + top: auto; + bottom: 0; } + .ng-toast.ng-toast--bottom .ng-toast__list { + top: auto; + bottom: 0; } + .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list { + pointer-events: none; } + .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert { + pointer-events: auto; } + .ng-toast.ng-toast--right .ng-toast__list { + left: auto; + right: 0; + margin-right: 20px; } + .ng-toast.ng-toast--right .ng-toast__message { + text-align: right; } + .ng-toast.ng-toast--left .ng-toast__list { + right: auto; + left: 0; + margin-left: 20px; } + .ng-toast.ng-toast--left .ng-toast__message { + text-align: left; } + .ng-toast .ng-toast__list { + display: inline-block; + position: absolute; + right: 0; + left: 0; + margin: 0 auto; + padding: 0; + list-style: none; } + .ng-toast .ng-toast__message { + display: block; + width: 100%; + text-align: center; } + .ng-toast .ng-toast__message .alert { + display: inline-block; } + .ng-toast .ng-toast__message__count { + display: inline-block; + margin: 0 15px 0 5px; } diff --git a/awx/ui/client/lib/ngToast/dist/ngToast.js b/awx/ui/client/lib/ngToast/dist/ngToast.js new file mode 100644 index 0000000000..ed94e0d321 --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast.js @@ -0,0 +1,284 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +(function(window, angular, undefined) { + 'use strict'; + + angular.module('ngToast.provider', []) + .provider('ngToast', [ + function() { + var messages = [], + messageStack = []; + + var defaults = { + animation: false, + className: 'success', + additionalClasses: null, + dismissOnTimeout: true, + timeout: 4000, + dismissButton: false, + dismissButtonHtml: '×', + dismissOnClick: true, + onDismiss: null, + compileContent: false, + combineDuplications: false, + horizontalPosition: 'right', // right, center, left + verticalPosition: 'top', // top, bottom, + maxNumber: 0, + newestOnTop: true + }; + + function Message(msg) { + var id = Math.floor(Math.random()*1000); + while (messages.indexOf(id) > -1) { + id = Math.floor(Math.random()*1000); + } + + this.id = id; + this.count = 0; + this.animation = defaults.animation; + this.className = defaults.className; + this.additionalClasses = defaults.additionalClasses; + this.dismissOnTimeout = defaults.dismissOnTimeout; + this.timeout = defaults.timeout; + this.dismissButton = defaults.dismissButton; + this.dismissButtonHtml = defaults.dismissButtonHtml; + this.dismissOnClick = defaults.dismissOnClick; + this.onDismiss = defaults.onDismiss; + this.compileContent = defaults.compileContent; + + angular.extend(this, msg); + } + + this.configure = function(config) { + angular.extend(defaults, config); + }; + + this.$get = [function() { + var _createWithClassName = function(className, msg) { + msg = (typeof msg === 'object') ? msg : {content: msg}; + msg.className = className; + + return this.create(msg); + }; + + return { + settings: defaults, + messages: messages, + dismiss: function(id) { + if (id) { + for (var i = messages.length - 1; i >= 0; i--) { + if (messages[i].id === id) { + messages.splice(i, 1); + messageStack.splice(messageStack.indexOf(id), 1); + return; + } + } + + } else { + while(messages.length > 0) { + messages.pop(); + } + messageStack = []; + } + }, + create: function(msg) { + msg = (typeof msg === 'object') ? msg : {content: msg}; + + if (defaults.combineDuplications) { + for (var i = messageStack.length - 1; i >= 0; i--) { + var _msg = messages[i]; + var _className = msg.className || 'success'; + + if (_msg.content === msg.content && + _msg.className === _className) { + messages[i].count++; + return; + } + } + } + + if (defaults.maxNumber > 0 && + messageStack.length >= defaults.maxNumber) { + this.dismiss(messageStack[0]); + } + + var newMsg = new Message(msg); + messages[defaults.newestOnTop ? 'unshift' : 'push'](newMsg); + messageStack.push(newMsg.id); + + return newMsg.id; + }, + success: function(msg) { + return _createWithClassName.call(this, 'success', msg); + }, + info: function(msg) { + return _createWithClassName.call(this, 'info', msg); + }, + warning: function(msg) { + return _createWithClassName.call(this, 'warning', msg); + }, + danger: function(msg) { + return _createWithClassName.call(this, 'danger', msg); + } + }; + }]; + } + ]); + +})(window, window.angular); + +(function(window, angular) { + 'use strict'; + + angular.module('ngToast.directives', ['ngToast.provider']) + .run(['$templateCache', + function($templateCache) { + $templateCache.put('ngToast/toast.html', + '
' + + '' + + '
'); + $templateCache.put('ngToast/toastMessage.html', + '
  • ' + + '
    ' + + '' + + '' + + '{{count + 1}}' + + '' + + '' + + '
    ' + + '
  • '); + } + ]) + .directive('toast', ['ngToast', '$templateCache', '$log', + function(ngToast, $templateCache, $log) { + return { + replace: true, + restrict: 'EA', + templateUrl: 'ngToast/toast.html', + compile: function(tElem, tAttrs) { + if (tAttrs.template) { + var template = $templateCache.get(tAttrs.template); + if (template) { + tElem.replaceWith(template); + } else { + $log.warn('ngToast: Provided template could not be loaded. ' + + 'Please be sure that it is populated before the element is represented.'); + } + } + + return function(scope) { + scope.hPos = ngToast.settings.horizontalPosition; + scope.vPos = ngToast.settings.verticalPosition; + scope.animation = ngToast.settings.animation; + scope.messages = ngToast.messages; + }; + } + }; + } + ]) + .directive('toastMessage', ['$timeout', '$compile', 'ngToast', + function($timeout, $compile, ngToast) { + return { + replace: true, + transclude: true, + restrict: 'EA', + scope: { + message: '=', + count: '=' + }, + controller: ['$scope', 'ngToast', function($scope, ngToast) { + $scope.dismiss = function() { + ngToast.dismiss($scope.message.id); + }; + }], + templateUrl: 'ngToast/toastMessage.html', + link: function(scope, element, attrs, ctrl, transclude) { + element.attr('data-message-id', scope.message.id); + + var dismissTimeout; + var scopeToBind = scope.message.compileContent; + + scope.cancelTimeout = function() { + $timeout.cancel(dismissTimeout); + }; + + scope.startTimeout = function() { + if (scope.message.dismissOnTimeout) { + dismissTimeout = $timeout(function() { + ngToast.dismiss(scope.message.id); + }, scope.message.timeout); + } + }; + + scope.onMouseEnter = function() { + scope.cancelTimeout(); + }; + + scope.onMouseLeave = function() { + scope.startTimeout(); + }; + + if (scopeToBind) { + var transcludedEl; + + transclude(scope, function(clone) { + transcludedEl = clone; + element.children().append(transcludedEl); + }); + + $timeout(function() { + $compile(transcludedEl.contents()) + (typeof scopeToBind === 'boolean' ? + scope.$parent : scopeToBind, function(compiledClone) { + transcludedEl.replaceWith(compiledClone); + }); + }, 0); + } + + scope.startTimeout(); + + if (scope.message.dismissOnClick) { + element.bind('click', function() { + ngToast.dismiss(scope.message.id); + scope.$apply(); + }); + } + + if (scope.message.onDismiss) { + scope.$on('$destroy', + scope.message.onDismiss.bind(scope.message)); + } + } + }; + } + ]); + +})(window, window.angular); + +(function(window, angular) { + 'use strict'; + + angular + .module('ngToast', [ + 'ngSanitize', + 'ngToast.directives', + 'ngToast.provider' + ]); + +})(window, window.angular); diff --git a/awx/ui/client/lib/ngToast/dist/ngToast.min.css b/awx/ui/client/lib/ngToast/dist/ngToast.min.css new file mode 100644 index 0000000000..715f089fc1 --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast.min.css @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast{position:fixed;z-index:1080;width:100%;height:0;margin-top:20px;text-align:center}.ng-toast.ng-toast--top,.ng-toast.ng-toast--top .ng-toast__list{top:0;bottom:auto}.ng-toast.ng-toast--top.ng-toast--center .ng-toast__list{position:static}.ng-toast.ng-toast--bottom,.ng-toast.ng-toast--bottom .ng-toast__list{top:auto;bottom:0}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list{pointer-events:none}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert{pointer-events:auto}.ng-toast.ng-toast--right .ng-toast__list{left:auto;right:0;margin-right:20px}.ng-toast.ng-toast--right .ng-toast__message{text-align:right}.ng-toast.ng-toast--left .ng-toast__list{right:auto;left:0;margin-left:20px}.ng-toast.ng-toast--left .ng-toast__message{text-align:left}.ng-toast .ng-toast__list{display:inline-block;position:absolute;right:0;left:0;margin:0 auto;padding:0;list-style:none}.ng-toast .ng-toast__message{display:block;width:100%;text-align:center}.ng-toast .ng-toast__message .alert{display:inline-block}.ng-toast .ng-toast__message__count{display:inline-block;margin:0 15px 0 5px} \ No newline at end of file diff --git a/awx/ui/client/lib/ngToast/dist/ngToast.min.js b/awx/ui/client/lib/ngToast/dist/ngToast.min.js new file mode 100644 index 0000000000..ecd44bf512 --- /dev/null +++ b/awx/ui/client/lib/ngToast/dist/ngToast.min.js @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +!function(a,b,c){"use strict";b.module("ngToast.provider",[]).provider("ngToast",[function(){function a(a){for(var d=Math.floor(1e3*Math.random());c.indexOf(d)>-1;)d=Math.floor(1e3*Math.random());this.id=d,this.count=0,this.animation=e.animation,this.className=e.className,this.additionalClasses=e.additionalClasses,this.dismissOnTimeout=e.dismissOnTimeout,this.timeout=e.timeout,this.dismissButton=e.dismissButton,this.dismissButtonHtml=e.dismissButtonHtml,this.dismissOnClick=e.dismissOnClick,this.onDismiss=e.onDismiss,this.compileContent=e.compileContent,b.extend(this,a)}var c=[],d=[],e={animation:!1,className:"success",additionalClasses:null,dismissOnTimeout:!0,timeout:4e3,dismissButton:!1,dismissButtonHtml:"×",dismissOnClick:!0,onDismiss:null,compileContent:!1,combineDuplications:!1,horizontalPosition:"right",verticalPosition:"top",maxNumber:0,newestOnTop:!0};this.configure=function(a){b.extend(e,a)},this.$get=[function(){var b=function(a,b){return b="object"==typeof b?b:{content:b},b.className=a,this.create(b)};return{settings:e,messages:c,dismiss:function(a){if(a){for(var b=c.length-1;b>=0;b--)if(c[b].id===a)return c.splice(b,1),void d.splice(d.indexOf(a),1)}else{for(;c.length>0;)c.pop();d=[]}},create:function(b){if(b="object"==typeof b?b:{content:b},e.combineDuplications)for(var f=d.length-1;f>=0;f--){var g=c[f],h=b.className||"success";if(g.content===b.content&&g.className===h)return void c[f].count++}e.maxNumber>0&&d.length>=e.maxNumber&&this.dismiss(d[0]);var i=new a(b);return c[e.newestOnTop?"unshift":"push"](i),d.push(i.id),i.id},success:function(a){return b.call(this,"success",a)},info:function(a){return b.call(this,"info",a)},warning:function(a){return b.call(this,"warning",a)},danger:function(a){return b.call(this,"danger",a)}}}]}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast.directives",["ngToast.provider"]).run(["$templateCache",function(a){a.put("ngToast/toast.html",'
    '),a.put("ngToast/toastMessage.html",'
  • {{count + 1}}
  • ')}]).directive("toast",["ngToast","$templateCache","$log",function(a,b,c){return{replace:!0,restrict:"EA",templateUrl:"ngToast/toast.html",compile:function(d,e){if(e.template){var f=b.get(e.template);f?d.replaceWith(f):c.warn("ngToast: Provided template could not be loaded. Please be sure that it is populated before the element is represented.")}return function(b){b.hPos=a.settings.horizontalPosition,b.vPos=a.settings.verticalPosition,b.animation=a.settings.animation,b.messages=a.messages}}}}]).directive("toastMessage",["$timeout","$compile","ngToast",function(a,b,c){return{replace:!0,transclude:!0,restrict:"EA",scope:{message:"=",count:"="},controller:["$scope","ngToast",function(a,b){a.dismiss=function(){b.dismiss(a.message.id)}}],templateUrl:"ngToast/toastMessage.html",link:function(d,e,f,g,h){e.attr("data-message-id",d.message.id);var i,j=d.message.compileContent;if(d.cancelTimeout=function(){a.cancel(i)},d.startTimeout=function(){d.message.dismissOnTimeout&&(i=a(function(){c.dismiss(d.message.id)},d.message.timeout))},d.onMouseEnter=function(){d.cancelTimeout()},d.onMouseLeave=function(){d.startTimeout()},j){var k;h(d,function(a){k=a,e.children().append(k)}),a(function(){b(k.contents())("boolean"==typeof j?d.$parent:j,function(a){k.replaceWith(a)})},0)}d.startTimeout(),d.message.dismissOnClick&&e.bind("click",function(){c.dismiss(d.message.id),d.$apply()}),d.message.onDismiss&&d.$on("$destroy",d.message.onDismiss.bind(d.message))}}}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast",["ngSanitize","ngToast.directives","ngToast.provider"])}(window,window.angular); \ No newline at end of file diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 9e6b67b1d4..efd17c9661 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -112,6 +112,7 @@ var tower = angular.module('Tower', [ JobTemplates.name, portalMode.name, search.name, + 'ngToast', 'templates', 'Utilities', 'OrganizationFormDefinition', @@ -210,15 +211,22 @@ var tower = angular.module('Tower', [ .config(['$pendolyticsProvider', function($pendolyticsProvider) { $pendolyticsProvider.doNotAutoStart(); }]) + .config(['ngToastProvider', function(ngToastProvider) { + ngToastProvider.configure({ + animation: 'slide', + dismissOnTimeout: true, + timeout: 4000 + }); + }]) .config(['$stateProvider', '$urlRouterProvider', '$breadcrumbProvider', '$urlMatcherFactoryProvider', function ($stateProvider, $urlRouterProvider, $breadcrumbProvider, $urlMatcherFactoryProvider) { - $urlMatcherFactoryProvider.strictMode(false) + $urlMatcherFactoryProvider.strictMode(false); $breadcrumbProvider.setOptions({ templateUrl: urlPrefix + 'partials/breadcrumb.html' }); // route to the details pane of /job/:id/host-event/:eventId if no other child specified - $urlRouterProvider.when('/jobs/*/host-event/*', '/jobs/*/host-event/*/details') + $urlRouterProvider.when('/jobs/*/host-event/*', '/jobs/*/host-event/*/details'); // $urlRouterProvider.otherwise("/home"); $urlRouterProvider.otherwise(function($injector){ var $state = $injector.get("$state"); diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 994667141a..a603f69bf2 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -517,7 +517,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, ReturnToCaller, GetProjectPath, Authorization, CredentialList, LookUpInit, GetChoices, Empty, DebugForm, Wait, SchedulesControllerInit, SchedulesListInit, SchedulesList, ProjectUpdate, $state, CreateSelect2, - OrganizationList) { + OrganizationList, NotificationsListInit, ToggleNotification) { ClearScope('htmlTemplate'); @@ -604,6 +604,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; Wait('stop'); + + NotificationsListInit({ + scope: $scope, + url: GetBasePath('projects'), + id: $scope.project_obj.id + }); }); if ($scope.removeChoicesReady) { @@ -710,6 +716,24 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, callback: 'choicesReady' }); + $scope.toggleNotification = function(event, id, column) { + var notifier = this.notification; + try { + $(event.target).tooltip('hide'); + } + catch(e) { + // ignore + } + ToggleNotification({ + scope: $scope, + url: $scope.project_url, + id: $scope.project_obj.id, + notifier: notifier, + column: column, + callback: 'NotificationRefresh' + }); + }; + // Save changes to the parent $scope.formSave = function () { var fld, i, params; @@ -820,5 +844,6 @@ ProjectsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'GetProjectPath', 'Authorization', 'CredentialList', 'LookUpInit', 'GetChoices', 'Empty', 'DebugForm', 'Wait', 'SchedulesControllerInit', 'SchedulesListInit', - 'SchedulesList', 'ProjectUpdate', '$state', 'CreateSelect2', 'OrganizationList' + 'SchedulesList', 'ProjectUpdate', '$state', 'CreateSelect2', + 'OrganizationList', 'NotificationsListInit', 'ToggleNotification' ]; diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js index 22d2e6f3fe..781e9459f0 100644 --- a/awx/ui/client/src/forms/Credentials.js +++ b/awx/ui/client/src/forms/Credentials.js @@ -122,7 +122,7 @@ export default ask: false, clear: false, hasShowInputButton: true, - apiField: 'passwowrd', + apiField: 'password', subForm: 'credentialSubForm' }, security_token: { diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 1d12f85734..24cd5965ca 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -11,7 +11,7 @@ */ export default - angular.module('JobTemplateFormDefinition', ['SchedulesListDefinition', 'CompletedJobsDefinition']) + angular.module('JobTemplateFormDefinition', [ 'CompletedJobsDefinition']) .value ('JobTemplateFormObject', { @@ -327,6 +327,10 @@ export default class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8' } } + }, + "notifications": { + include: "NotificationsList" + } }, @@ -339,19 +343,23 @@ export default permissions: { iterator: 'permission', url: urls.access_list + }, + notifications: { + iterator: 'notification', + url: '/api/v1/notifiers/' } }; } }) - .factory('JobTemplateForm', ['JobTemplateFormObject', 'SchedulesList', 'CompletedJobsList', - function(JobTemplateFormObject, SchedulesList, CompletedJobsList) { + .factory('JobTemplateForm', ['JobTemplateFormObject', 'NotificationsList', 'CompletedJobsList', + function(JobTemplateFormObject, NotificationsList, CompletedJobsList) { return function() { var itm; for (itm in JobTemplateFormObject.related) { - if (JobTemplateFormObject.related[itm].include === "SchedulesList") { - JobTemplateFormObject.related[itm] = SchedulesList; + if (JobTemplateFormObject.related[itm].include === "NotificationsList") { + JobTemplateFormObject.related[itm] = NotificationsList; JobTemplateFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list } if (JobTemplateFormObject.related[itm].include === "CompletedJobsList") { diff --git a/awx/ui/client/src/forms/Organizations.js b/awx/ui/client/src/forms/Organizations.js index 14b0a5d9e1..18ff618a57 100644 --- a/awx/ui/client/src/forms/Organizations.js +++ b/awx/ui/client/src/forms/Organizations.js @@ -12,7 +12,7 @@ export default angular.module('OrganizationFormDefinition', []) - .value('OrganizationForm', { + .value('OrganizationFormObject', { addTitle: 'New Organization', //Title in add mode editTitle: '{{ name }}', //Title in edit mode @@ -171,6 +171,10 @@ export default class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8' } } + }, + "notifications": { + include: "NotificationsList" + } }, @@ -179,8 +183,25 @@ export default permissions: { iterator: 'permission', url: urls.access_list + }, + notifications: { + iterator: 'notification', + url: '/api/v1/notifiers/' } }; } + }) - }); //OrganizationForm + .factory('OrganizationForm', ['OrganizationFormObject', 'NotificationsList', + function(OrganizationFormObject, NotificationsList) { + return function() { + var itm; + for (itm in OrganizationFormObject.related) { + if (OrganizationFormObject.related[itm].include === "NotificationsList") { + OrganizationFormObject.related[itm] = NotificationsList; + OrganizationFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list + } + } + return OrganizationFormObject; + }; + }]); diff --git a/awx/ui/client/src/forms/Projects.js b/awx/ui/client/src/forms/Projects.js index 05e40e6d08..ad391c941e 100644 --- a/awx/ui/client/src/forms/Projects.js +++ b/awx/ui/client/src/forms/Projects.js @@ -43,9 +43,6 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) type: 'lookup', sourceModel: 'organization', sourceField: 'name', - addRequired: true, - editRequired: false, - excludeMode: 'edit', ngClick: 'lookUpOrganization()', awRequiredWhen: { variable: "organizationrequired", @@ -151,17 +148,6 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) editRequired: false, subForm: 'sourceSubForm' }, - organization: { - label: 'Organization', - type: 'lookup', - sourceModel: 'organization', - sourceField: 'name', - ngClick: 'lookUpOrganization()', - awRequiredWhen: { - variable: "organizationrequired", - init: "true" - } - }, credential: { label: 'SCM Credential', type: 'lookup', @@ -277,6 +263,9 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8' } } + }, + "notifications": { + include: "NotificationsList" } }, @@ -285,18 +274,22 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) permissions: { iterator: 'permission', url: urls.access_list + }, + notifications: { + iterator: 'notification', + url: '/api/v1/notifiers/' } }; } }) - .factory('ProjectsForm', ['ProjectsFormObject', 'SchedulesList', function(ProjectsFormObject, ScheduleList) { + .factory('ProjectsForm', ['ProjectsFormObject', 'NotificationsList', function(ProjectsFormObject, NotificationsList) { return function() { var itm; for (itm in ProjectsFormObject.related) { - if (ProjectsFormObject.related[itm].include === "SchedulesList") { - ProjectsFormObject.related[itm] = ScheduleList; + if (ProjectsFormObject.related[itm].include === "NotificationsList") { + ProjectsFormObject.related[itm] = NotificationsList; ProjectsFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list } } diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js index 8613341836..fc9ca38979 100644 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ b/awx/ui/client/src/helpers/JobTemplates.js @@ -127,7 +127,7 @@ angular.module('JobTemplatesHelper', ['Utilities']) scope.setCallbackHelp(); scope.callback_url = scope.callback_server_path + ((data.related.callback) ? data.related.callback : - GetBasePath('job_templates') + id + '/callback/'); + GetBasePath('job_templates') + id + '/callback/'); master.callback_url = scope.callback_url; scope.can_edit = data.summary_fields.can_edit; diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index 0083262f19..502ec7df71 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -21,6 +21,7 @@ export default 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', 'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', 'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', + 'ToggleNotification', 'NotificationsListInit', function( $filter, $scope, $rootScope, $compile, $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, @@ -30,7 +31,7 @@ export default Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit, SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state, - CreateSelect2 + CreateSelect2, ToggleNotification, NotificationsListInit ) { ClearScope(); @@ -60,6 +61,12 @@ export default id: id }); + NotificationsListInit({ + scope: $scope, + url: GetBasePath('job_templates'), + id: id + }); + callback = function() { // Make sure the form controller knows there was a change $scope[form.name + '_form'].$setDirty(); @@ -122,6 +129,24 @@ export default } }; + $scope.toggleNotification = function(event, notifier_id, column) { + var notifier = this.notification; + try { + $(event.target).tooltip('hide'); + } + catch(e) { + // ignore + } + ToggleNotification({ + scope: $scope, + url: defaultUrl, + id: id, + notifier: notifier, + column: column, + callback: 'NotificationRefresh' + }); + }; + $scope.toggleScanInfo = function() { $scope.project_name = 'Default'; if($scope.project === null){ @@ -506,7 +531,7 @@ export default .error(function(res, status){ ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); - }); + }); } else { $state.go('jobTemplates'); diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index 88048941ff..e7940a1fe4 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -6,61 +6,125 @@ export default [ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait', - 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', - 'GenerateForm', 'SearchInit' , 'PaginateInit', - 'LookUpInit', 'OrganizationList', '$scope', '$state', + 'NotificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', + 'GenerateForm', 'SearchInit' , 'PaginateInit', 'LookUpInit', + 'OrganizationList', '$scope', '$state', 'CreateSelect2', 'GetChoices', + 'NotificationsTypeChange', function( $rootScope, pagination, $compile, SchedulerInit, Rest, Wait, - notificationsFormObject, ProcessErrors, GetBasePath, Empty, - GenerateForm, SearchInit, PaginateInit, - LookUpInit, OrganizationList, $scope, $state + NotificationsFormObject, ProcessErrors, GetBasePath, Empty, + GenerateForm, SearchInit, PaginateInit, LookUpInit, + OrganizationList, $scope, $state, CreateSelect2, GetChoices, + NotificationsTypeChange ) { - var scope = $scope, - generator = GenerateForm, - form = notificationsFormObject, - url = GetBasePath('notifications'); + var generator = GenerateForm, + form = NotificationsFormObject, + url = GetBasePath('notifiers'); generator.inject(form, { mode: 'add' , - scope:scope, + scope: $scope, related: false }); generator.reset(); + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function () { + var i; + for (i = 0; i < $scope.notification_type_options.length; i++) { + if ($scope.notification_type_options[i].value === '') { + $scope.notification_type_options[i].value="manual"; + // $scope.scm_type = $scope.scm_type_options[i]; + break; + } + } + CreateSelect2({ + element: '#notifier_notification_type', + multiple: false + }); + }); + LookUpInit({ url: GetBasePath('organization'), - scope: scope, + scope: $scope, form: form, list: OrganizationList, field: 'organization', input_type: 'radio' }); - // Save - scope.formSave = function () { + GetChoices({ + scope: $scope, + url: url, + field: 'notification_type', + variable: 'notification_type_options', + callback: 'choicesReady' + }); - generator.clearApiErrors(); - Wait('start'); - Rest.setUrl(url); - Rest.post({ - name: scope.name, - description: scope.description, - organization: scope.organization, - script: scope.script - }) - .success(function (data) { - $rootScope.addedItem = data.id; - $state.go('inventoryScripts', {}, {reload: true}); - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new inventory script. POST returned status: ' + status }); + $scope.typeChange = function () { + for(var fld in form.fields){ + if(form.fields[fld] && form.fields[fld].subForm){ + $scope[fld] = null; + $scope.notifier_form[fld].$setPristine(); + } + } + + NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) { + $scope[field[0]] = field[1]; }); }; - scope.formCancel = function () { - $state.transitionTo('inventoryScripts'); + // Save + $scope.formSave = function () { + var params, + // config, + v = $scope.notification_type.value; + + generator.clearApiErrors(); + params = { + "name" : $scope.name, + "description": $scope.description, + "organization": $scope.organization, + "notification_type" : v, + "notification_configuration": {} + }; + + function processValue(value, i , field){ + if(field.type === 'textarea'){ + $scope[i] = $scope[i].toString().split('\n'); + } + if(field.type === 'checkbox'){ + $scope[i] = Boolean($scope[i]); + } + if(field.type === 'number'){ + $scope[i] = Number($scope[i]); + } + return $scope[i]; + + } + + params.notification_configuration = _.object(Object.keys(form.fields) + .filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1)) + .map(i => [i, processValue($scope[i], i , form.fields[i])])); + + Wait('start'); + Rest.setUrl(url); + Rest.post(params) + .success(function (data) { + $rootScope.addedItem = data.id; + $state.go('notifications', {}, {reload: true}); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new notifier. POST returned status: ' + status }); + }); + }; + + $scope.formCancel = function () { + $state.transitionTo('notifications'); }; } diff --git a/awx/ui/client/src/notifications/add/add.route.js b/awx/ui/client/src/notifications/add/add.route.js index 6bad062844..7b464e2edb 100644 --- a/awx/ui/client/src/notifications/add/add.route.js +++ b/awx/ui/client/src/notifications/add/add.route.js @@ -18,6 +18,6 @@ export default { }, ncyBreadcrumb: { parent: 'notifications', - label: 'Create Notification' + label: 'Create Notifier' } }; diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index a14b3334af..d1fc1102e6 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -6,91 +6,179 @@ export default [ 'Rest', 'Wait', - 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', + 'NotificationsFormObject', 'ProcessErrors', 'GetBasePath', 'GenerateForm', 'SearchInit' , 'PaginateInit', - 'LookUpInit', 'OrganizationList', 'inventory_script', - '$scope', '$state', + 'LookUpInit', 'OrganizationList', 'notifier', + '$scope', '$state', 'GetChoices', 'CreateSelect2', 'Empty', + '$rootScope', 'NotificationsTypeChange', function( Rest, Wait, - notificationsFormObject, ProcessErrors, GetBasePath, + NotificationsFormObject, ProcessErrors, GetBasePath, GenerateForm, SearchInit, PaginateInit, - LookUpInit, OrganizationList, inventory_script, - $scope, $state + LookUpInit, OrganizationList, notifier, + $scope, $state, GetChoices, CreateSelect2, Empty, + $rootScope, NotificationsTypeChange ) { var generator = GenerateForm, - id = inventory_script.id, - form = notificationsFormObject, + id = notifier.id, + form = NotificationsFormObject, master = {}, - url = GetBasePath('notifications'); + url = GetBasePath('notifiers'); - $scope.inventory_script = inventory_script; + $scope.notifier = notifier; generator.inject(form, { mode: 'edit' , scope:$scope, - related: false, - activityStream: false + related: false + }); + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function () { + var i; + for (i = 0; i < $scope.notification_type_options.length; i++) { + if ($scope.notification_type_options[i].value === '') { + $scope.notification_type_options[i].value="manual"; + // $scope.scm_type = $scope.scm_type_options[i]; + break; + } + } + + Wait('start'); + Rest.setUrl(url + id+'/'); + Rest.get() + .success(function (data) { + var fld; + for (fld in form.fields) { + if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = data[fld]; + } + + if(data.notification_configuration[fld]){ + $scope[fld] = data.notification_configuration[fld]; + master[fld] = data.notification_configuration[fld]; + + if(form.fields[fld].type === 'textarea'){ + $scope[fld] = $scope[fld].toString().replace(',' , '\n'); + } + } + + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + data.notification_type = (Empty(data.notification_type)) ? '' : data.notification_type; + for (var i = 0; i < $scope.notification_type_options.length; i++) { + if ($scope.notification_type_options[i].value === data.notification_type) { + $scope.notification_type = $scope.notification_type_options[i]; + break; + } + } + + // if ($scope.notification_type.value !== 'manual') { + // $scope.pathRequired = false; + // $scope.scmRequired = true; + // } else { + // $scope.pathRequired = true; + // $scope.scmRequired = false; + // } + + master.notification_type = $scope.notification_type; + CreateSelect2({ + element: '#notifier_notification_type', + multiple: false + }); + NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) { + $scope[field[0]] = field[1]; + }); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to retrieve notification: ' + id + '. GET status: ' + status }); + }); }); - generator.reset(); LookUpInit({ url: GetBasePath('organization'), scope: $scope, form: form, - // hdr: "Select Custom Inventory", list: OrganizationList, field: 'organization', input_type: 'radio' }); - // Retrieve detail record and prepopulate the form - Wait('start'); - Rest.setUrl(url + id+'/'); - Rest.get() - .success(function (data) { - var fld; - for (fld in form.fields) { - if (data[fld]) { - $scope[fld] = data[fld]; - master[fld] = data[fld]; - } + GetChoices({ + scope: $scope, + url: url, + field: 'notification_type', + variable: 'notification_type_options', + callback: 'choicesReady' + }); - if (form.fields[fld].sourceModel && data.summary_fields && - data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - } + $scope.typeChange = function () { + for(var fld in form.fields){ + if(form.fields[fld] && form.fields[fld].subForm){ + $scope[fld] = null; + $scope.notifier_form[fld].$setPristine(); } + } + + NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) { + $scope[field[0]] = field[1]; + }); + }; + + $scope.formSave = function(){ + var params, + // config, + v = $scope.notification_type.value; + + generator.clearApiErrors(); + params = { + "name" : $scope.name, + "description": $scope.description, + "organization": $scope.organization, + "notification_type" : v, + "notification_configuration": {} + }; + + function processValue(value, i , field){ + if(field.type === 'textarea'){ + $scope[i] = $scope[i].toString().split('\n'); + } + if(field.type === 'checkbox'){ + $scope[i] = Boolean($scope[i]); + } + return $scope[i]; + + } + + params.notification_configuration = _.object(Object.keys(form.fields) + .filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1)) + .map(i => [i, processValue($scope[i], i , form.fields[i])])); + + Wait('start'); + Rest.setUrl(url+ id+'/'); + Rest.put(params) + .success(function (data) { + $rootScope.addedItem = data.id; + $state.go('notifications', {}, {reload: true}); Wait('stop'); }) .error(function (data, status) { ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to retrieve inventory script: ' + id + '. GET status: ' + status }); + msg: 'Failed to add new notifier. POST returned status: ' + status }); }); - - $scope.formSave = function () { - generator.clearApiErrors(); - Wait('start'); - Rest.setUrl(url+ id+'/'); - Rest.put({ - name: $scope.name, - description: $scope.description, - organization: $scope.organization, - script: $scope.script - }) - .success(function () { - $state.transitionTo('inventoryScriptsList'); - Wait('stop'); - - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new inventory script. PUT returned status: ' + status }); - }); }; + $scope.formCancel = function () { - $state.transitionTo('inventoryScripts'); + $state.transitionTo('notifications'); }; } diff --git a/awx/ui/client/src/notifications/edit/edit.route.js b/awx/ui/client/src/notifications/edit/edit.route.js index 1987b32cad..1e1228c4c2 100644 --- a/awx/ui/client/src/notifications/edit/edit.route.js +++ b/awx/ui/client/src/notifications/edit/edit.route.js @@ -8,16 +8,44 @@ import {templateUrl} from '../../shared/template-url/template-url.factory'; export default { name: 'notifications.edit', - route: '/edit', + route: '/:notifier_id', templateUrl: templateUrl('notifications/edit/edit'), controller: 'notificationsEditController', resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); - }] + }], + notifier: + [ '$state', + '$stateParams', + '$q', + 'Rest', + 'GetBasePath', + 'ProcessErrors', + function($state, $stateParams, $q, rest, getBasePath, ProcessErrors) { + if ($stateParams.notifier) { + return $q.when($stateParams.notifier); + } + + var notifierId = $stateParams.notifier_id; + + var url = getBasePath('notifiers') + notifierId + '/'; + rest.setUrl(url); + return rest.get() + .then(function(data) { + return data.data; + }).catch(function (response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory script info. GET returned status: ' + + response.status + }); + }); + } + ] }, ncyBreadcrumb: { parent: 'notifications', - label: 'Edit Notification' + label: 'Edit Notifier' } }; diff --git a/awx/ui/client/src/notifications/list/list.controller.js b/awx/ui/client/src/notifications/list/list.controller.js deleted file mode 100644 index df0ef9f7d2..0000000000 --- a/awx/ui/client/src/notifications/list/list.controller.js +++ /dev/null @@ -1,83 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default - [ '$rootScope','Wait', 'generateList', 'notificationsListObject', - 'GetBasePath' , 'SearchInit' , 'PaginateInit', - 'Rest' , 'ProcessErrors', 'Prompt', '$state', - function( - $rootScope,Wait, GenerateList, notificationsListObject, - GetBasePath, SearchInit, PaginateInit, - Rest, ProcessErrors, Prompt, $state - ) { - var scope = $rootScope.$new(), - defaultUrl = GetBasePath('notifications'), - list = notificationsListObject, - view = GenerateList; - - view.inject( list, { - mode: 'edit', - scope: scope - }); - - // SearchInit({ - // scope: scope, - // set: 'notifications', - // list: list, - // url: defaultUrl - // }); - // - // if ($rootScope.addedItem) { - // scope.addedItem = $rootScope.addedItem; - // delete $rootScope.addedItem; - // } - // PaginateInit({ - // scope: scope, - // list: list, - // url: defaultUrl - // }); - // - // scope.search(list.iterator); - - scope.editNotification = function(){ - $state.transitionTo('notifications.edit',{ - inventory_script_id: this.inventory_script.id, - inventory_script: this.inventory_script - }); - }; - - scope.deleteNotification = function(id, name){ - - var action = function () { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function () { - scope.search(list.iterator); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); - }); - }; - - var bodyHtml = '
    Are you sure you want to delete the inventory script below?
    ' + name + '
    '; - Prompt({ - hdr: 'Delete', - body: bodyHtml, - action: action, - actionText: 'DELETE' - }); - }; - - scope.addNotification = function(){ - $state.transitionTo('notifications.add'); - }; - - } - ]; diff --git a/awx/ui/client/src/notifications/main.js b/awx/ui/client/src/notifications/main.js index 147b3b8479..31106454eb 100644 --- a/awx/ui/client/src/notifications/main.js +++ b/awx/ui/client/src/notifications/main.js @@ -5,18 +5,26 @@ *************************************************/ -import notificationsList from './list/main'; +import notificationTemplatesList from './notification-templates-list/main'; import notificationsAdd from './add/main'; import notificationsEdit from './edit/main'; -import list from './notifications.list'; -import form from './notifications.form'; +import list from './notificationTemplates.list'; +import form from './notificationTemplates.form'; +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'; export default angular.module('notifications', [ - notificationsList.name, + notificationTemplatesList.name, notificationsAdd.name, notificationsEdit.name ]) - .factory('notificationsListObject', list) - .factory('notificationsFormObject', form); + .factory('NotificationTemplatesList', list) + .factory('NotificationsFormObject', form) + .factory('NotificationsList', notificationsList) + .factory('ToggleNotification', toggleNotification) + .factory('NotificationsListInit', notificationsListInit) + .service('NotificationsTypeChange', typeChange); diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js new file mode 100644 index 0000000000..c6041b891a --- /dev/null +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -0,0 +1,212 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$rootScope','Wait', 'generateList', 'NotificationTemplatesList', + 'GetBasePath' , 'SearchInit' , 'PaginateInit', 'Rest' , + 'ProcessErrors', 'Prompt', '$state', 'GetChoices', 'Empty', 'Find', + 'ngToast', '$compile', '$filter', + function( + $rootScope,Wait, GenerateList, NotificationTemplatesList, + GetBasePath, SearchInit, PaginateInit, Rest, + ProcessErrors, Prompt, $state, GetChoices, Empty, Find, ngToast, + $compile, $filter) { + var scope = $rootScope.$new(), + defaultUrl = GetBasePath('notifiers'), + list = NotificationTemplatesList, + view = GenerateList; + + view.inject( list, { + mode: 'edit', + scope: scope + }); + + + if (scope.removePostRefresh) { + scope.removePostRefresh(); + } + scope.removePostRefresh = scope.$on('PostRefresh', function () { + Wait('stop'); + if (scope.notifiers) { + scope.notifiers.forEach(function(notifier, i) { + scope.notification_type_options.forEach(function(type) { + if (type.value === notifier.notification_type) { + scope.notifiers[i].notification_type = type.label; + scope.notifiers[i].status = notifier.summary_fields.recent_notifications[0].status; + } + }); + }); + } + }); + + if (scope.removeChoicesHere) { + scope.removeChoicesHere(); + } + scope.removeChoicesHere = scope.$on('choicesReadyNotifierList', function () { + list.fields.notification_type.searchOptions = scope.notification_type_options; + + SearchInit({ + scope: scope, + set: 'notifiers', + list: list, + url: defaultUrl + }); + + if ($rootScope.addedItem) { + scope.addedItem = $rootScope.addedItem; + delete $rootScope.addedItem; + } + PaginateInit({ + scope: scope, + list: list, + url: defaultUrl + }); + + scope.search(list.iterator); + + }); + + GetChoices({ + scope: scope, + url: defaultUrl, + field: 'notification_type', + variable: 'notification_type_options', + callback: 'choicesReadyNotifierList' + }); + + function attachElem(event, html, title) { + var elem = $(event.target).parent(); + try { + elem.tooltip('hide'); + elem.popover('destroy'); + } + catch(err) { + //ignore + } + + $('.popover').each(function() { + // remove lingering popover
    . Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + elem.attr({ + "aw-pop-over": html, + "data-popover-title": title, + "data-placement": "right" }); + $compile(elem)(scope); + elem.on('shown.bs.popover', function() { + $('.popover').each(function() { + // $compile($(this))(scope); //make nested directives work! + }); + $('.popover-content, .popover-title').click(function() { + elem.popover('hide'); + }); + }); + elem.popover('show'); + } + + scope.showSummary = function(event, id) { + + if (!Empty(id)) { + var recent_notifications, + html, title = "Recent Notifications"; + + scope.notifiers.forEach(function(notifier){ + if(notifier.id === id){ + recent_notifications = notifier.summary_fields.recent_notifications; + } + }); + Wait('stop'); + if (recent_notifications.length > 0) { + html = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + html += "\n"; + + recent_notifications.forEach(function(row) { + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + }); + html += "\n"; + html += "
    StatusTime
    " + ($filter('longDate')(row.created)).replace(/ /,'
    ') + "
    \n"; + } + else { + html = "

    No recent notifications.

    \n"; + } + attachElem(event, html, title); + } + }; + + + + + scope.testNotification = function(){ + var name = this.notifier.name; + Rest.setUrl(defaultUrl + this.notifier.id +'/test/'); + Rest.post({}) + .then(function () { + ngToast.success({ + content: `Test Notification Success: ${name} ` + }); + + }) + .catch(function () { + ngToast.danger({ + content: 'Test Notification Failure' + }); + }); + }; + + scope.addNotification = function(){ + $state.transitionTo('notifications.add'); + }; + + scope.editNotification = function(){ + $state.transitionTo('notifications.edit',{ + notifier_id: this.notifier.id, + notifier: this.notifier + }); + }; + + scope.deleteNotification = function(id, name){ + + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function () { + scope.search(list.iterator); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + var bodyHtml = '
    Are you sure you want to delete the inventory below?
    ' + name + '
    '; + Prompt({ + hdr: 'Delete', + body: bodyHtml, + action: action, + actionText: 'DELETE' + }); + }; + + + } + ]; diff --git a/awx/ui/client/src/notifications/list/list.partial.html b/awx/ui/client/src/notifications/notification-templates-list/list.partial.html similarity index 100% rename from awx/ui/client/src/notifications/list/list.partial.html rename to awx/ui/client/src/notifications/notification-templates-list/list.partial.html diff --git a/awx/ui/client/src/notifications/list/list.route.js b/awx/ui/client/src/notifications/notification-templates-list/list.route.js similarity index 77% rename from awx/ui/client/src/notifications/list/list.route.js rename to awx/ui/client/src/notifications/notification-templates-list/list.route.js index da76880791..e783b1e2f1 100644 --- a/awx/ui/client/src/notifications/list/list.route.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.route.js @@ -9,8 +9,8 @@ import {templateUrl} from '../../shared/template-url/template-url.factory'; export default { name: 'notifications', route: '/notifications', - templateUrl: templateUrl('notifications/list/list'), - controller: 'notificationsListController', + templateUrl: templateUrl('notifications/notification-templates-list/list'), + controller: 'notificationTemplatesListController', resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); diff --git a/awx/ui/client/src/notifications/list/main.js b/awx/ui/client/src/notifications/notification-templates-list/main.js similarity index 75% rename from awx/ui/client/src/notifications/list/main.js rename to awx/ui/client/src/notifications/notification-templates-list/main.js index 35cab03cef..2db7d37a09 100644 --- a/awx/ui/client/src/notifications/list/main.js +++ b/awx/ui/client/src/notifications/notification-templates-list/main.js @@ -8,8 +8,8 @@ import route from './list.route'; import controller from './list.controller'; export default - angular.module('notificationsList', []) - .controller('notificationsListController', controller) + angular.module('notificationTemplatesList', []) + .controller('notificationTemplatesListController', controller) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js new file mode 100644 index 0000000000..eeef07d1c7 --- /dev/null +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -0,0 +1,351 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:CustomInventory + * @description This form is for adding/editing an organization +*/ + +export default function() { + return { + + addTitle: 'New Notification Template', + editTitle: '{{ name }}', + name: 'notifier', + showActions: true, + subFormTitles: { + typeSubForm: 'Type Details', + }, + + fields: { + name: { + label: 'Name', + type: 'text', + addRequired: true, + editRequired: true, + capitalize: false + }, + description: { + label: 'Description', + type: 'text', + addRequired: false, + editRequired: false + }, + organization: { + label: 'Organization', + type: 'lookup', + sourceModel: 'organization', + sourceField: 'name', + ngClick: 'lookUpOrganization()', + awRequiredWhen: { + variable: "organizationrequired", + init: "true" + } + }, + notification_type: { + label: 'Type', + type: 'select', + addRequired: true, + editRequired: true, + class: 'NotificationsForm-typeSelect prepend-asterisk', + ngOptions: 'type.label for type in notification_type_options track by type.value', + ngChange: 'typeChange()', + hasSubForm: true + }, + username: { + label: 'Username', + type: 'text', + awRequiredWhen: { + variable: "email_required", + init: "false" + }, + ngShow: "notification_type.value == 'email' ", + subForm: 'typeSubForm' + }, + use_tls: { + label: 'Use TLS', + type: 'checkbox', + ngShow: "notification_type.value == 'email' ", + subForm: 'typeSubForm' + }, + host: { + label: 'Host', + type: 'text', + awRequiredWhen: { + variable: "email_required", + init: "false" + }, + ngShow: "notification_type.value == 'email' ", + subForm: 'typeSubForm' + }, + sender: { + label: 'Sender Email', + type: 'text', + awRequiredWhen: { + variable: "email_required", + init: "false" + }, + ngShow: "notification_type.value == 'email' ", + subForm: 'typeSubForm' + }, + recipients: { + label: 'Recipient List', + type: 'textarea', + rows: 3, + awPopOver: '

    Type an option on each line.

    '+ + '

    For example:
    alias1@email.com
    \n alias2@email.com
    \n', + dataTitle: 'Recipient List', + dataPlacement: 'right', + dataContainer: "body", + awRequiredWhen: { + variable: "email_required", + init: "false" + }, + ngShow: "notification_type.value == 'email' ", + subForm: 'typeSubForm' + }, + password: { + labelBind: 'passwordLabel', + type: 'sensitive', + hasShowInputButton: true, + awRequiredWhen: { + variable: "password_required" , + init: "false" + }, + // ngRequired:"notification_type.value == 'email' || notification_type.value == 'irc'", + ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ", + subForm: 'typeSubForm' + }, + use_ssl: { + labelBind: 'sslLabel', + type: 'checkbox', + ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ", + subForm: 'typeSubForm' + }, + port: { + labelBind: 'portLabel', + type: 'number', + integer: true, + spinner: true, + 'class': "input-small", + min: 0, + awRequiredWhen: { + variable: "port_required", + init: "false" + }, + ngShow: "notification_type.value == 'email' || notification_type.value == 'irc'", + subForm: 'typeSubForm' + }, + channels: { + label: 'Destination Channels', + type: 'textarea', + rows: 3, + awPopOver: '

    Type an option on each line. The pound symbol (#) is not required.

    '+ + '

    For example:
    engineering
    \n support
    \n', + dataTitle: 'Destination Channels', + dataPlacement: 'right', + dataContainer: "body", + awRequiredWhen: { + variable: "channel_required", + init: "false" + }, + ngShow: "notification_type.value == 'slack' || notification_type.value == 'hipchat'", + subForm: 'typeSubForm' + }, + token: { + labelBind: 'tokenLabel', + type: 'sensitive', + hasShowInputButton: true, + awRequiredWhen: { + variable: "token_required", + init: "false" + }, + // ngRequired:"notification_type.value == 'slack' || notification_type.value == 'pagerduty' || notification_type.value == 'hipchat'", + ngShow: "notification_type.value == 'slack' || notification_type.value == 'pagerduty' || notification_type.value == 'hipchat'", + subForm: 'typeSubForm' + }, + account_token: { + label: 'Account Token', + type: 'sensitive', + hasShowInputButton: true, + awRequiredWhen: { + variable: "twilio_required", + init: "false" + }, + ngShow: "notification_type.value == 'twilio' ", + subForm: 'typeSubForm' + }, + from_number: { + label: 'Source Phone Number', + type: 'text', + awRequiredWhen: { + variable: "twilio_required", + init: "false" + }, + ngShow: "notification_type.value == 'twilio' ", + subForm: 'typeSubForm' + }, + to_numbers: { + label: 'Destination SMS Number', + type: 'textarea', + rows: 3, + awPopOver: '

    Type an option on each line.

    '+ + '

    For example:
    alias1@email.com
    \n alias2@email.com
    \n', + dataTitle: 'Destination Channels', + dataPlacement: 'right', + dataContainer: "body", + awRequiredWhen: { + variable: "twilio_required", + init: "false" + }, + ngShow: "notification_type.value == 'twilio' ", + subForm: 'typeSubForm' + }, + account_sid: { + label: 'Account SID', + type: 'text', + awRequiredWhen: { + variable: "twilio_required", + init: "false" + }, + ngShow: "notification_type.value == 'twilio' ", + subForm: 'typeSubForm' + }, + subdomain: { + label: 'Pagerduty subdomain', + type: 'text', + awRequiredWhen: { + variable: "pagerduty_required", + init: "false" + }, + ngShow: "notification_type.value == 'pagerduty' ", + subForm: 'typeSubForm' + }, + service_key: { + label: 'API Service/Integration Key', + type: 'text', + awRequiredWhen: { + variable: "pagerduty_required", + init: "false" + }, + ngShow: "notification_type.value == 'pagerduty' ", + subForm: 'typeSubForm' + }, + client_name: { + label: 'Client Identifier', + type: 'text', + awRequiredWhen: { + variable: "pagerduty_required", + init: "false" + }, + ngShow: "notification_type.value == 'pagerduty' ", + subForm: 'typeSubForm' + }, + message_from: { + label: 'Label to be shown with notification', + type: 'text', + awRequiredWhen: { + variable: "hipchat_required", + init: "false" + }, + ngShow: "notification_type.value == 'hipchat' ", + subForm: 'typeSubForm' + }, + api_url: { + label: 'API URL (e.g: https://mycompany.hiptchat.com)', + type: 'text', + awRequiredWhen: { + variable: "hipchat_required", + init: "false" + }, + ngShow: "notification_type.value == 'hipchat' ", + subForm: 'typeSubForm' + }, + color: { + label: 'Notification Color', + type: 'text', + awRequiredWhen: { + variable: "hipchat_required", + init: "false" + }, + ngShow: "notification_type.value == 'hipchat' ", + subForm: 'typeSubForm' + }, + notify: { + label: 'Notify Channel', + type: 'text', + awRequiredWhen: { + variable: "hipchat_required", + init: "false" + }, + ngShow: "notification_type.value == 'hipchat' ", + subForm: 'typeSubForm' + }, + url: { + label: 'Target URL', + type: 'text', + awRequiredWhen: { + variable: "webhook_required", + init: "false" + }, + ngShow: "notification_type.value == 'webhook' ", + subForm: 'typeSubForm' + }, + headers: { + label: 'HTTP Headers', + type: 'text', + awRequiredWhen: { + variable: "webhook_required", + init: "false" + }, + ngShow: "notification_type.value == 'webhook' ", + subForm: 'typeSubForm' + }, + server: { + label: 'IRC Server Address', + type: 'text', + awRequiredWhen: { + variable: "irc_required", + init: "false" + }, + ngShow: "notification_type.value == 'irc' ", + subForm: 'typeSubForm' + }, + nickname: { + label: 'IRC Nick', + type: 'text', + awRequiredWhen: { + variable: "irc_required", + init: "false" + }, + ngShow: "notification_type.value == 'irc' ", + subForm: 'typeSubForm' + }, + targets: { + label: 'Destination Channels or Users', + type: 'text', + awRequiredWhen: { + variable: "irc_required", + init: "false" + }, + ngShow: "notification_type.value == 'irc' ", + subForm: 'typeSubForm' + }, + + }, + + buttons: { //for now always generates