1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 15:21:13 +03:00

Merge pull request #1451 from jaredevantabor/notifications

Adding notifications into the UI
This commit is contained in:
Jared Tabor 2016-04-12 16:11:13 -07:00
commit eee91eea86
41 changed files with 2013 additions and 313 deletions

View File

@ -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
}

View File

@ -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
<link rel="stylesheet" href="bower/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="bower/ngtoast/dist/ngToast.min.css">
<script src="bower/angular-sanitize/angular-sanitize.min.js"></script>
<script src="bower/ngtoast/dist/ngToast.min.js"></script>
```
*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
<body>
<toast></toast>
...
</body>
```
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
<script src="bower/angular-animate/angular-animate.min.js"></script>
```
**Built-in**
1. Include the ngToast animation stylesheet:
```html
<link rel="stylesheet" href="bower/ngtoast/dist/ngToast-animations.min.css">
```
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

View File

@ -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"
]
}

View File

@ -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; }

View File

@ -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}

View File

@ -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; }

View File

@ -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: '&times;',
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',
'<div class="ng-toast ng-toast--{{hPos}} ng-toast--{{vPos}} {{animation ? \'ng-toast--animate-\' + animation : \'\'}}">' +
'<ul class="ng-toast__list">' +
'<toast-message ng-repeat="message in messages" ' +
'message="message" count="message.count">' +
'<span ng-bind-html="message.content"></span>' +
'</toast-message>' +
'</ul>' +
'</div>');
$templateCache.put('ngToast/toastMessage.html',
'<li class="ng-toast__message {{message.additionalClasses}}"' +
'ng-mouseenter="onMouseEnter()"' +
'ng-mouseleave="onMouseLeave()">' +
'<div class="alert alert-{{message.className}}" ' +
'ng-class="{\'alert-dismissible\': message.dismissButton}">' +
'<button type="button" class="close" ' +
'ng-if="message.dismissButton" ' +
'ng-bind-html="message.dismissButtonHtml" ' +
'ng-click="!message.dismissOnClick && dismiss()">' +
'</button>' +
'<span ng-if="count" class="ng-toast__message__count">' +
'{{count + 1}}' +
'</span>' +
'<span ng-if="!message.compileContent" ng-transclude></span>' +
'</div>' +
'</li>');
}
])
.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 <toast> 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);

View File

@ -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}

View File

@ -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:"&times;",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",'<div class="ng-toast ng-toast--{{hPos}} ng-toast--{{vPos}} {{animation ? \'ng-toast--animate-\' + animation : \'\'}}"><ul class="ng-toast__list"><toast-message ng-repeat="message in messages" message="message" count="message.count"><span ng-bind-html="message.content"></span></toast-message></ul></div>'),a.put("ngToast/toastMessage.html",'<li class="ng-toast__message {{message.additionalClasses}}"ng-mouseenter="onMouseEnter()"ng-mouseleave="onMouseLeave()"><div class="alert alert-{{message.className}}" ng-class="{\'alert-dismissible\': message.dismissButton}"><button type="button" class="close" ng-if="message.dismissButton" ng-bind-html="message.dismissButtonHtml" ng-click="!message.dismissOnClick && dismiss()"></button><span ng-if="count" class="ng-toast__message__count">{{count + 1}}</span><span ng-if="!message.compileContent" ng-transclude></span></div></li>')}]).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 <toast> 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);

View File

@ -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");

View File

@ -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'
];

View File

@ -122,7 +122,7 @@ export default
ask: false,
clear: false,
hasShowInputButton: true,
apiField: 'passwowrd',
apiField: 'password',
subForm: 'credentialSubForm'
},
security_token: {

View File

@ -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") {

View File

@ -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;
};
}]);

View File

@ -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
}
}

View File

@ -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;

View File

@ -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');

View File

@ -6,61 +6,123 @@
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";
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,
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');
};
}

View File

@ -18,6 +18,6 @@ export default {
},
ncyBreadcrumb: {
parent: 'notifications',
label: 'Create Notification'
label: 'Create Notifier'
}
};

View File

@ -6,91 +6,169 @@
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";
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;
}
}
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,
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');
};
}

View File

@ -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'
}
};

View File

@ -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 = '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory script below?</div><div class="Prompt-bodyTarget">' + name + '</div>';
Prompt({
hdr: 'Delete',
body: bodyHtml,
action: action,
actionText: 'DELETE'
});
};
scope.addNotification = function(){
$state.transitionTo('notifications.add');
};
}
];

View File

@ -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);

View File

@ -0,0 +1,204 @@
/*************************************************
* 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 <div>. 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 = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Time</th>";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
recent_notifications.forEach(function(row) {
html += "<tr>\n";
html += "<td><i class=\"fa icon-job-" + row.status + "\"></i></td>\n";
html += "<td>" + ($filter('longDate')(row.created)).replace(/ /,'<br />') + "</td>\n";
html += "</tr>\n";
});
html += "</tbody>\n";
html += "</table>\n";
}
else {
html = "<p>No recent notifications.</p>\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: `<i class="fa fa-check-circle Toast-successIcon"></i> Test Notification Success: <b>${name}</b> `,
});
})
.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 = '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory below?</div><div class="Prompt-bodyTarget">' + name + '</div>';
Prompt({
hdr: 'Delete',
body: bodyHtml,
action: action,
actionText: 'DELETE'
});
};
}
];

View File

@ -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();

View File

@ -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);
}]);

View File

@ -0,0 +1,349 @@
/*************************************************
* 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: '<p>Type an option on each line.</p>'+
'<p>For example:<br>alias1@email.com<br>\n alias2@email.com<br>\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"
},
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: '<p>Type an option on each line. The pound symbol (#) is not required.</p>'+
'<p>For example:<br>engineering<br>\n support<br>\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"
},
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: '<p>Type an option on each line.</p>'+
'<p>For example:<br>alias1@email.com<br>\n alias2@email.com<br>\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 <button> tags
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
},
cancel: {
ngClick: 'formCancel()',
}
}
};
}

View File

@ -0,0 +1,88 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function(){
return {
name: 'notifiers' ,
listTitle: 'Notification Templates',
iterator: 'notifier',
index: false,
hover: false,
fields: {
status: {
label: '',
columnClass: 'List-staticColumn--smallStatus',
searchable: false,
nosort: true,
ngClick: "null",
iconOnly: true,
excludeModal: true,
icons: [{
icon: "{{ 'icon-job-' + notifier.status }}",
awToolTip: "Click for recent notifications",
awTipPlacement: "right",
ngClick: "showSummary($event, notifier.id)",
ngClass: ""
}]
},
name: {
key: true,
label: 'Name',
columnClass: 'col-md-3 col-sm-9 col-xs-9',
linkTo: '/#/notifications/{{notifier.id}}'
},
notification_type: {
label: 'Type',
searchType: 'select',
searchOptions: [], // will be set by Options call to projects resource
excludeModal: true,
columnClass: 'col-md-4 hidden-sm hidden-xs'
}
},
actions: {
add: {
mode: 'all', // One of: edit, select, all
ngClick: 'addNotification()',
awToolTip: 'Create a new custom inventory',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fieldActions: {
columnClass: 'col-md-2 col-sm-3 col-xs-3',
test: {
ngClick: "testNotification(notification.id)",
icon: 'fa-bell-o',
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Test notification',
dataPlacement: 'top'
},
edit: {
ngClick: "editNotification(notification.id)",
icon: 'fa-edit',
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Edit notification',
dataPlacement: 'top'
},
"delete": {
ngClick: "deleteNotification(notifier.id, notifier.name)",
icon: 'fa-trash',
label: 'Delete',
"class": 'btn-sm',
awToolTip: 'Delete notification',
dataPlacement: 'top'
}
}
};
}

View File

@ -0,0 +1,18 @@
.NotificationsForm-typeSelect{
flex: none;
}
.NotifierList-lastColumn{
text-align: left!important;
}
.alert-success{
background-color: #3cb878;
color: #fff;
}
.Toast-successIcon{
font-size: x-large;
vertical-align: middle;
padding-right: 10px;
}

View File

@ -1,47 +0,0 @@
/*************************************************
* 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',
editTitle: '{{ name }}',
name: 'notification',
showActions: true,
fields: {
name: {
label: 'Name',
type: 'text',
addRequired: true,
editRequired: true,
capitalize: false
},
description: {
label: 'Description',
type: 'text',
addRequired: false,
editRequired: false
}
},
buttons: { //for now always generates <button> tags
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: true //Disable when $pristine or $invalid, optional
},
cancel: {
ngClick: 'formCancel()',
}
}
};
}

View File

@ -4,12 +4,10 @@
* All Rights Reserved
*************************************************/
export default function(){
return {
name: 'notifications' ,
listTitle: 'Notifications',
title: 'Notifications',
iterator: 'notification',
index: false,
hover: false,
@ -19,45 +17,39 @@ export default function(){
key: true,
label: 'Name',
columnClass: 'col-md-3 col-sm-9 col-xs-9',
modalColumnClass: 'col-md-8'
linkTo: '/#/notifications/{{notifier.id}}'
},
description: {
label: 'Description',
notification_type: {
label: 'Type',
searchType: 'select',
searchOptions: [],
excludeModal: true,
columnClass: 'col-md-4 hidden-sm hidden-xs'
}
},
actions: {
add: {
mode: 'all', // One of: edit, select, all
ngClick: 'addNotification()',
awToolTip: 'Create a new custom inventory',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fieldActions: {
columnClass: 'col-md-2 col-sm-3 col-xs-3',
edit: {
ngClick: "editNotification(inventory_script.id)",
icon: 'fa-edit',
label: 'Edit',
"class": 'btn-sm',
awToolTip: 'Edit credential',
dataPlacement: 'top'
},
"delete": {
ngClick: "deleteNotification(notification.id, notification.name)",
icon: 'fa-trash',
label: 'Delete',
"class": 'btn-sm',
awToolTip: 'Delete credential',
dataPlacement: 'top'
notifiers_success: {
label: 'Successful',
flag: 'notifiers_success',
type: "toggle",
ngClick: "toggleNotification($event, notification.id, \"notifiers_success\")",
awToolTip: "{{ schedule.play_tip }}",
dataTipWatch: "schedule.play_tip",
dataPlacement: "right",
searchable: false,
nosort: true,
},
notifiers_error: {
label: 'Failed',
columnClass: 'NotifierList-lastColumn',
flag: 'notifiers_error',
type: "toggle",
ngClick: "toggleNotification($event, notification.id, \"notifiers_error\")",
awToolTip: "{{ schedule.play_tip }}",
dataTipWatch: "schedule.play_tip",
dataPlacement: "right",
searchable: false,
nosort: true,
}
}
};
}

View File

@ -0,0 +1,57 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* For setting the values and choices for notification lists
*
* NotificationsListInit({
* scope: scope,
* id: notification.id to update
* url: notifier url off of related object
* });
*
*/
export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest',
function(Wait, GetBasePath, ProcessErrors, Rest) {
return function(params) {
var scope = params.scope,
url = params.url,
id = params.id;
if (scope.relatednotificationsRemove) {
scope.relatednotificationsRemove();
}
scope.relatednotificationsRemove = scope.$on('relatednotifications', function () {
var columns = ['/notifiers_success/', '/notifiers_error/'];
_.map(columns, function(column){
var notifier_url = url + id + column;
Rest.setUrl(notifier_url);
Rest.get()
.success( function(data, i, j, obj) {
var type = (obj.url.indexOf('success')>0) ? "notifiers_success" : "notifiers_error";
if (data.results) {
_.forEach(data.results, function(result){
_.forEach(scope.notifications, function(notification){
if(notification.id === result.id){
notification[type] = true;
}
});
});
}
else {
Wait('stop');
}
})
.error( function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to update notification ' + data.id + ' PUT returned: ' + status });
});
});
});
};
}];

View File

@ -0,0 +1,55 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* Flip a notifications's enable flag
*
* ToggleNotification({
* scope: scope,
* id: schedule.id to update
* callback: scope.$emit label to call when update completes
* });
*
*/
export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest',
function(Wait, GetBasePath, ProcessErrors, Rest) {
return function(params) {
var scope = params.scope,
notifier = params.notifier,
id = params.id,
notifier_id = params.notifier.id,
callback = params.callback,
column = params.column, // notifiers_success/notifiers_error
url = params.url+ id+ "/"+ column + '/';
if(!notifier[column]){
params = {
id: notifier_id
};
}
else {
params = {
id: notifier_id,
disassociate: 1
};
}
Rest.setUrl(url);
Rest.post(params)
.success( function(data) {
if (callback) {
scope.$emit(callback, data.id);
notifier[column] = !notifier[column];
}
else {
Wait('stop');
}
})
.error( function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to update notification ' + data.id + ' PUT returned: ' + status });
});
};
}];

View File

@ -0,0 +1,73 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default [
function () {
return{
getDetailFields: function(type) {
var obj = {};
obj.email_required = false;
obj.slack_required = false;
obj.hipchat_required = false;
obj.pagerduty_required = false;
obj.irc_required = false;
obj.twilio_required = false;
obj.webhook_required = false;
obj.token_required = false;
obj.port_required = false;
obj.password_required = false;
obj.channel_required = false;
switch (type) {
case 'email':
obj.portLabel = ' Port';
obj.sslLabel = ' Use SSL';
obj.passwordLabel = ' Password';
obj.email_required = true;
obj.port_required = true;
obj.password_required = true;
break;
case 'slack':
obj.tokenLabel =' Token';
obj.slack_required = true;
obj.token_required = true;
obj.channel_required = true;
break;
case 'hipchat':
obj.tokenLabel = ' Token';
obj.hipchat_required = true;
obj.channel_required = true;
obj.token_required = true;
break;
case 'twilio':
obj.twilio_required = true;
break;
case 'webhook':
obj.webhook_required = true;
break;
case 'pagerduty':
obj.tokenLabel = ' API Token';
obj.pagerduty_required = true;
obj.token_required = true;
break;
case 'irc':
obj.portLabel = ' IRC Server Port';
obj.sslLabel = ' SSL Connection';
obj.passwordLabel = ' IRC Server Password';
obj.irc_required = true;
obj.password_required = true;
obj.port_required = true;
break;
}
// make object an array of tuples
return Object.keys(obj)
.map(i => [i, obj[i]]);
}
};
}];

View File

@ -7,16 +7,17 @@
export default ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt',
'ClearScope', 'GetBasePath', 'Wait', '$state',
'ClearScope', 'GetBasePath', 'Wait', '$state', 'NotificationsListInit',
'ToggleNotification',
function($scope, $rootScope, $compile, $location, $log,
$stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors,
RelatedSearchInit, RelatedPaginateInit, Prompt, ClearScope, GetBasePath,
Wait, $state) {
Wait, $state, NotificationsListInit, ToggleNotification) {
ClearScope();
// Inject dynamic view
var form = OrganizationForm,
var form = OrganizationForm(),
generator = GenerateForm,
defaultUrl = GetBasePath('organizations'),
base = $location.path().replace(/^\//, '').split('/')[0],
@ -42,6 +43,11 @@ export default ['$scope', '$rootScope', '$compile', '$location',
$scope.search(relatedSets[set].iterator);
}
Wait('stop');
NotificationsListInit({
scope: $scope,
url: defaultUrl,
id: id
});
});
// Retrieve detail record and prepopulate the form
@ -66,6 +72,7 @@ export default ['$scope', '$rootScope', '$compile', '$location',
};
}
}
angular.extend(relatedSets, form
.relatedSets(data.related));
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
@ -79,6 +86,23 @@ export default ['$scope', '$rootScope', '$compile', '$location',
msg: 'Failed to retrieve organization: ' + $stateParams.id + '. GET status: ' + status });
});
$scope.toggleNotification = function(event, id, column) {
var notifier = this.notification;
try {
$(event.target).tooltip('hide');
}
catch(e) {
// ignore
}
ToggleNotification({
scope: $scope,
url: defaultUrl,
id: $scope.organization_id,
notifier: notifier,
column: column,
callback: 'NotificationRefresh'
});
};
// Save changes to the parent
$scope.formSave = function () {
@ -150,4 +174,4 @@ export default ['$scope', '$rootScope', '$compile', '$location',
};
}
]
];

View File

@ -342,7 +342,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
var viewValue = elm.val(), label, validity = true;
if ( scope[attrs.awRequiredWhen] && (elm.attr('required') === null || elm.attr('required') === undefined) ) {
$(elm).attr('required','required');
if ($(elm).hasClass('lookup')) {
if ($(elm).hasClass('lookup') || $(elm).hasClass('ui-spinner-input')) {
$(elm).parent().parent().parent().find('label').first().addClass('prepend-asterisk');
}
else {

View File

@ -1155,6 +1155,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += (field.readonly) ? "readonly " : "";
html += (field.integer) ? "integer " : "";
html += (field.disabled) ? "data-disabled=\"true\" " : "";
html += (field.awRequiredWhen) ? "data-awrequired-init=\"" + field.awRequiredWhen.init + "\" aw-required-when=\"" +
field.awRequiredWhen.variable + "\" " : "";
html += " >\n";
// Add error messages
if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired)) {
@ -1217,13 +1219,15 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
}
html += "<div class=\"checkbox\">\n";
html += "<label ";
html += (field.labelBind) ? "ng-bind=\"" + field.labelBind + "\" " : "";
//html += "for=\"" + fld + '">';
html += ">";
html += "<label>";
html += buildCheckbox(this.form, field, fld, undefined, false);
html += (field.icon) ? Icon(field.icon) : "";
html += '<span class=\"Form-inputLabel\">' + field.label + "</span>";
if (field.labelBind) {
html += "\t\t<span class=\"Form-inputLabel\" ng-bind=\"" + field.labelBind + "\">\n\t\t</span>";
} else {
html += "<span class=\"Form-inputLabel\">" + field.label + "</span>";
}
html += (field.awPopOver) ? this.attr(field, 'awPopOver', fld) : "";
html += "</label>\n";
html += "<div class=\"error api-error\" id=\"" + this.form.name + "-" + fld + "-api-error\" ng-bind=\"" +

View File

@ -178,6 +178,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
case 'job_details':
icon = 'fa-list-ul';
break;
case 'test':
icon = 'fa-bell-o';
break;
case 'copy':
icon = "fa-copy";
break;
@ -461,7 +464,13 @@ angular.module('GeneratorHelpers', [systemStatus.name])
html += "<td class=\"List-tableCell " + fld + "-column";
html += (field['class']) ? " " + field['class'] : "";
html += " " + field.columnClass;
html += "\"><div class='ScheduleToggle' ng-class='{\"is-on\": " + list.iterator + ".enabled\}' aw-tool-tip='" + field.awToolTip + "' data-placement='" + field.dataPlacement + "' data-tip-watch='" + field.dataTipWatch + "'><div ng-show='" + list.iterator + ".enabled' class='ScheduleToggle-switch is-on' ng-click='" + field.ngClick + "'>ON</div><div ng-show='!" + list.iterator + ".enabled' class='ScheduleToggle-switch' ng-click='" + field.ngClick + "'>OFF</div></div></td>";
html += "\"><div class='ScheduleToggle' ng-class='{\"is-on\": " + list.iterator + ".";
html += (field.flag) ? field.flag : "enabled";
html += "\}' aw-tool-tip='" + field.awToolTip + "' data-placement='" + field.dataPlacement + "' data-tip-watch='" + field.dataTipWatch + "'><div ng-show='" + list.iterator + "." ;
html += (field.flag) ? field.flag : 'enabled';
html += "' class='ScheduleToggle-switch is-on' ng-click='" + field.ngClick + "'>ON</div><div ng-show='!" + list.iterator + "." ;
html += (field.flag) ? field.flag : "enabled";
html += "' class='ScheduleToggle-switch' ng-click='" + field.ngClick + "'>OFF</div></div></td>";
} else {
html += "<td class=\"List-tableCell " + fld + "-column";
html += (field['class']) ? " " + field['class'] : "";

View File

@ -308,7 +308,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
html += "<div class=\"List-titleText\">" + list.listTitle + "</div>";
// We want to show the list title badge by default and only hide it when the list config specifically passes a false flag
list.listTitleBadge = (typeof list.listTitleBadge === 'boolean' && list.listTitleBadge == false) ? false : true;
list.listTitleBadge = (typeof list.listTitleBadge === 'boolean' && list.listTitleBadge === false) ? false : true;
if(list.listTitleBadge) {
html += "<span class=\"badge List-titleBadge\">{{(" + list.iterator + "_total_rows | number:0)}}</span>";
}

View File

@ -15,6 +15,7 @@
<link rel="stylesheet" href="{{ STATIC_URL }}lib/codemirror/theme/elegant.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}lib/codemirror/addon/lint/lint.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}lib/nvd3/build/nv.d3.css" type="text/css">
<link rel="stylesheet" href="{{ STATIC_URL }}lib/ngToast/dist/ngToast.min.css" type="text/css">
<link rel="stylesheet" href="{{ STATIC_URL }}tower.min.css?v={{version}}" type="text/css">
@ -40,6 +41,7 @@
<main-menu></main-menu>
<bread-crumb></bread-crumb>
<toast></toast>
<div class="container-fluid" id="content-container">
<div class="row">
@ -219,7 +221,7 @@
<div class="overlay"></div>
<div class="spinny"><i class="fa fa-cog fa-spin fa-2x"></i> <p>working...</p></div>
</div>
</div>
<tower-footer></tower-footer>
<script>
// HACK: Need this to support global-dependent