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:
commit
eee91eea86
59
awx/ui/client/lib/ngToast/.bower.json
Normal file
59
awx/ui/client/lib/ngToast/.bower.json
Normal 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
|
||||
}
|
119
awx/ui/client/lib/ngToast/README.md
Normal file
119
awx/ui/client/lib/ngToast/README.md
Normal 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
|
49
awx/ui/client/lib/ngToast/bower.json
Normal file
49
awx/ui/client/lib/ngToast/bower.json
Normal 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"
|
||||
]
|
||||
}
|
107
awx/ui/client/lib/ngToast/dist/ngToast-animations.css
vendored
Normal file
107
awx/ui/client/lib/ngToast/dist/ngToast-animations.css
vendored
Normal 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; }
|
7
awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css
vendored
Normal file
7
awx/ui/client/lib/ngToast/dist/ngToast-animations.min.css
vendored
Normal 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}
|
60
awx/ui/client/lib/ngToast/dist/ngToast.css
vendored
Normal file
60
awx/ui/client/lib/ngToast/dist/ngToast.css
vendored
Normal 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; }
|
284
awx/ui/client/lib/ngToast/dist/ngToast.js
vendored
Normal file
284
awx/ui/client/lib/ngToast/dist/ngToast.js
vendored
Normal 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: '×',
|
||||
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);
|
7
awx/ui/client/lib/ngToast/dist/ngToast.min.css
vendored
Normal file
7
awx/ui/client/lib/ngToast/dist/ngToast.min.css
vendored
Normal 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}
|
7
awx/ui/client/lib/ngToast/dist/ngToast.min.js
vendored
Normal file
7
awx/ui/client/lib/ngToast/dist/ngToast.min.js
vendored
Normal 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:"×",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);
|
@ -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");
|
||||
|
@ -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'
|
||||
];
|
||||
|
@ -122,7 +122,7 @@ export default
|
||||
ask: false,
|
||||
clear: false,
|
||||
hasShowInputButton: true,
|
||||
apiField: 'passwowrd',
|
||||
apiField: 'password',
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
security_token: {
|
||||
|
@ -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") {
|
||||
|
@ -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;
|
||||
};
|
||||
}]);
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ export default {
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'notifications',
|
||||
label: 'Create Notification'
|
||||
label: 'Create Notifier'
|
||||
}
|
||||
};
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
}
|
||||
];
|
@ -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);
|
||||
|
@ -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'
|
||||
});
|
||||
};
|
||||
}
|
||||
];
|
@ -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();
|
@ -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);
|
||||
}]);
|
349
awx/ui/client/src/notifications/notificationTemplates.form.js
Normal file
349
awx/ui/client/src/notifications/notificationTemplates.form.js
Normal 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()',
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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: '+ 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'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
18
awx/ui/client/src/notifications/notifications.block.less
Normal file
18
awx/ui/client/src/notifications/notifications.block.less
Normal 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;
|
||||
}
|
@ -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()',
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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: '+ 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,
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
}];
|
@ -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 });
|
||||
});
|
||||
};
|
||||
}];
|
@ -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]]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}];
|
@ -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',
|
||||
|
||||
};
|
||||
}
|
||||
]
|
||||
];
|
||||
|
@ -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 {
|
||||
|
@ -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=\"" +
|
||||
|
@ -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'] : "";
|
||||
|
@ -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>";
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user