diff --git a/awx/ui/static/html/event_log.html b/awx/ui/static/html/event_log.html
new file mode 100644
index 0000000000..e55e3ae972
--- /dev/null
+++ b/awx/ui/static/html/event_log.html
@@ -0,0 +1,618 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 2,
+ "url": "/api/v1/event_log/2/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/groups/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "inventory": {
+ "name": "Test Inventory",
+ "description": "Testing activity stream"
+ },
+ "object1": {
+ "name": "Group A",
+ "description": "The A group"
+ },
+ "user": {
+ "username": "chouseknecht"
+ },
+ "object2": {}
+ },
+
+ "created": "2013-11-05T15:18:55.000Z",
+ "modified": "2013-11-05T15:18:55.000Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:55.000Z",
+ "operation": "create",
+ "changes": {
+ "before": { "groups": [ "Group X", "Group Y", "Group Z" ] },
+ "after": { "groups": [ "Group A", "Group X", "Group Y", "Group Z" ] }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 3,
+ "url": "/api/v1/event_log/2/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/groups/N/children",
+ "object2": "/groups/N/"
+ },
+
+ "summary_fields": {
+ "inventory": {
+ "name": "Test Inventory",
+ "description": "Testing activity stream"
+ },
+ "object1": {
+ "name": "Group A",
+ "description": "The A group"
+ },
+ "user": { "username": "chouseknecht" },
+ "object2": {
+ "name": "Group B",
+ "description": "The B group"
+ }
+ },
+
+ "created": "2013-11-05T15:18:58.391Z",
+ "modified": "2013-11-05T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "associate",
+ "changes": {
+ "before": { "groups": [ "Group X", "Group Y", "Group Z" ] },
+ "after": { "groups": [ "Group A", "Group X", "Group Y", "Group Z" ] }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ },
+ {
+ "id": 1,
+ "url": "/api/v1/event_log/1/",
+ "related": {
+ "user": "/users/N/",
+ "object1": "/organizations/N/",
+ "object2": ""
+ },
+
+ "summary_fields": {
+ "object1": {
+ "name": "Frito Lay",
+ "description": "Salty Snacks"
+ },
+ "user": {
+ "username": "chouseknecht"
+ }
+ },
+
+ "created": "2013-11-06T15:18:58.391Z",
+ "modified": "2013-11-06T15:18:58.514Z",
+ "user": 1,
+ "event_time": "2013-11-06T15:18:58.514Z",
+ "operation": "change",
+ "changes": {
+ "before": { "description": "Healthy Snacks" },
+ "after": { "description": "Salty Snacks" }
+ },
+ "relationship": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/awx/ui/static/img/cow.png b/awx/ui/static/img/cow.png
deleted file mode 100644
index bd9ac11bf6..0000000000
Binary files a/awx/ui/static/img/cow.png and /dev/null differ
diff --git a/awx/ui/static/img/footsteps.png b/awx/ui/static/img/footsteps.png
new file mode 100644
index 0000000000..42bdfd90da
Binary files /dev/null and b/awx/ui/static/img/footsteps.png differ
diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js
index 75dabd23f7..c92697f086 100644
--- a/awx/ui/static/js/app.js
+++ b/awx/ui/static/js/app.js
@@ -5,7 +5,7 @@
*
*/
-var urlPrefix = '/static/';
+var urlPrefix = $basePath;
angular.module('ansible', [
'RestServices',
@@ -74,13 +74,16 @@ angular.module('ansible', [
'InventorySyncStatusWidget',
'SCMSyncStatusWidget',
'ObjectCountWidget',
+ 'StreamWidget',
'JobsHelper',
'InventoryStatusDefinition',
'InventorySummaryHelpDefinition',
'InventoryHostsHelpDefinition',
'TreeSelector',
'CredentialsHelper',
- 'TimerService'
+ 'TimerService',
+ 'StreamListDefinition',
+ 'HomeGroupListDefinition'
])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.
@@ -245,13 +248,15 @@ angular.module('ansible', [
when('/logout', { templateUrl: urlPrefix + 'partials/organizations.html', controller: Authenticate }).
when('/home', { templateUrl: urlPrefix + 'partials/home.html', controller: Home }).
+
+ when('/home/groups', { templateUrl: urlPrefix + 'partials/subhome.html', controller: HomeGroups }).
otherwise({redirectTo: '/home'});
}])
.run(['$cookieStore', '$rootScope', 'CheckLicense', '$location', 'Authorization','LoadBasePaths', 'ViewLicense',
- 'Timer',
+ 'Timer', 'ClearScope', 'HideStream',
function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense,
- Timer) {
+ Timer, ClearScope, HideStream) {
LoadBasePaths();
@@ -260,12 +265,17 @@ angular.module('ansible', [
$rootScope.sessionTimer = Timer.init();
$rootScope.$on("$routeChangeStart", function(event, next, current) {
+
+ // Before navigating away from current tab, make sure the primary view is visible
+ if ($('#stream-container').is(':visible')) {
+ HideStream();
+ }
+
// On each navigation request, check that the user is logged in
-
- var tst = /login/;
+ var tst = /(login|logout)/;
var path = $location.path();
if ( !tst.test($location.path()) ) {
- // capture most recent URL, excluding login
+ // capture most recent URL, excluding login/logout
$rootScope.lastPath = path;
$cookieStore.put('lastPath', path);
}
@@ -288,11 +298,6 @@ angular.module('ansible', [
CheckLicense();
}
- if ($rootScope.timer) {
- clearInterval($rootScope.timer);
- $rootScope.timer = null;
- }
-
// Make the correct tab active
var base = $location.path().replace(/^\//,'').split('/')[0];
if (base == '') {
diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js
index ae05097723..8ad5229ae3 100644
--- a/awx/ui/static/js/controllers/Credentials.js
+++ b/awx/ui/static/js/controllers/Credentials.js
@@ -227,6 +227,9 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa
data['username'] = scope['access_key'];
data['password'] = scope['secret_key'];
break;
+ case 'scm':
+ data['ssh_key_unlock'] = scope['scm_key_unlock'];
+ break;
}
if (Empty(data.team) && Empty(data.user)) {
@@ -415,7 +418,10 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP
scope['ssh_password'] = data.password;
master['ssh_username'] = scope['ssh_username'];
master['ssh_password'] = scope['ssh_password'];
- break;
+ break;
+ case 'scm':
+ scope['scm_key_unlock'] = data['ssh_key_unlock'];
+ break;
}
scope.$emit('credentialLoaded');
@@ -451,7 +457,6 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP
}
}
-
if (!Empty(scope.team)) {
data.team = scope.team;
data.user = "";
@@ -472,6 +477,9 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP
data['username'] = scope['access_key'];
data['password'] = scope['secret_key'];
break;
+ case 'scm':
+ data['ssh_key_unlock'] = scope['scm_key_unlock'];
+ break;
}
if (Empty(data.team) && Empty(data.user)) {
diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js
index b1e99a22c8..922f4e0fa5 100644
--- a/awx/ui/static/js/controllers/Home.js
+++ b/awx/ui/static/js/controllers/Home.js
@@ -11,7 +11,7 @@
'use strict';
function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, JobStatus, InventorySyncStatus, SCMSyncStatus,
- ClearScope) {
+ ClearScope, Stream) {
ClearScope('home'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope.
@@ -28,6 +28,8 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J
InventorySyncStatus({ target: 'container2' });
SCMSyncStatus({ target: 'container4' });
ObjectCount({ target: 'container3' });
+
+ $rootScope.showActivity = function() { Stream(); }
$rootScope.$on('WidgetLoaded', function() {
// Once all the widgets report back 'loaded', turn off Wait widget
@@ -39,4 +41,82 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J
}
Home.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', 'Wait', 'ObjectCount', 'JobStatus', 'InventorySyncStatus',
- 'SCMSyncStatus', 'ClearScope'];
\ No newline at end of file
+ 'SCMSyncStatus', 'ClearScope', 'Stream'];
+
+
+function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope,
+ GetBasePath, SearchInit, PaginateInit, FormatDate, HostsStatusMsg, UpdateStatusMsg, ViewUpdateStatus) {
+
+ ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
+ //scope.
+
+ var generator = GenerateList;
+ var list = HomeGroupList;
+ var defaultUrl=GetBasePath('groups');
+
+ var scope = generator.inject(list, { mode: 'edit' });
+ var base = $location.path().replace(/^\//,'').split('/')[0];
+
+ if (scope.removePostRefresh) {
+ scope.removePostRefresh();
+ }
+ scope.removePostRefresh = scope.$on('PostRefresh', function() {
+ var msg, update_status, last_update;
+ for (var i=0; i < scope.groups.length; i++) {
+
+ scope['groups'][i]['inventory_name'] = scope['groups'][i]['summary_fields']['inventory']['name'];
+
+ last_update = (scope.groups[i].summary_fields.inventory_source.last_updated == null) ? null :
+ FormatDate(new Date(scope.groups[i].summary_fields.inventory_source.last_updated));
+
+ // Set values for Failed Hosts column
+ scope.groups[i].failed_hosts = scope.groups[i].hosts_with_active_failures + ' / ' + scope.groups[i].total_hosts;
+
+ msg = HostsStatusMsg({
+ active_failures: scope.groups[i].hosts_with_active_failures,
+ total_hosts: scope.groups[i].total_hosts,
+ inventory_id: scope.groups[i].inventory
+ });
+
+ update_status = UpdateStatusMsg({ status: scope.groups[i].summary_fields.inventory_source.status });
+
+ scope.groups[i].failed_hosts_tip = msg['tooltip'];
+ scope.groups[i].failed_hosts_link = msg['url'];
+ scope.groups[i].failed_hosts_class = msg['class'];
+ scope.groups[i].status = update_status['status'];
+ scope.groups[i].source = scope.groups[i].summary_fields.inventory_source.source;
+ scope.groups[i].last_updated = last_update;
+ scope.groups[i].status_badge_class = update_status['class'];
+ scope.groups[i].status_badge_tooltip = update_status['tooltip'];
+ }
+ });
+
+ SearchInit({ scope: scope, set: 'groups', list: list, url: defaultUrl });
+ PaginateInit({ scope: scope, list: list, url: defaultUrl });
+
+ if ($routeParams['status']) {
+ // with status param, called post update-submit
+ scope[list.iterator + 'SearchField'] = 'status';
+ scope[list.iterator + 'SelectShow'] = true;
+ scope[list.iterator + 'SearchSelectOpts'] = list.fields['status'].searchOptions;
+ scope[list.iterator + 'SearchFieldLabel'] = list.fields['status'].label.replace(/\
/g,' ');
+ for (var opt in list.fields['status'].searchOptions) {
+ if (list.fields['status'].searchOptions[opt].value == $routeParams['status']) {
+ scope[list.iterator + 'SearchSelectValue'] = list.fields['status'].searchOptions[opt];
+ break;
+ }
+ }
+ }
+
+ scope.search(list.iterator);
+
+ LoadBreadCrumbs();
+
+ scope.viewUpdateStatus = function(id) { ViewUpdateStatus({ scope: scope, group_id: id }) };
+
+ }
+
+HomeGroups.$inject = [ '$location', '$routeParams', 'HomeGroupList', 'GenerateList', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller',
+ 'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'HostsStatusMsg', 'UpdateStatusMsg', 'ViewUpdateStatus'
+ ];
+
\ No newline at end of file
diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js
index bfe9635ff9..724a007d02 100644
--- a/awx/ui/static/js/controllers/Jobs.js
+++ b/awx/ui/static/js/controllers/Jobs.js
@@ -13,7 +13,7 @@
function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobList,
GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, LookUpInit, SubmitJob, FormatDate, Refresh,
- JobStatusToolTip)
+ JobStatusToolTip, Empty)
{
ClearScope('htmlTemplate');
var list = JobList;
@@ -52,7 +52,7 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest,
if ($routeParams['job_host_summaries__host']) {
defaultUrl += '?job_host_summaries__host=' + $routeParams['job_host_summaries__host'];
}
- if ($routeParams['inventory__int'] && $routeParams['status']) {
+ else if ($routeParams['inventory__int'] && $routeParams['status']) {
defaultUrl += '?inventory__int=' + $routeParams['inventory__int'] + '&status=' +
$routeParams['status'];
}
@@ -70,6 +70,18 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest,
scope[list.iterator + 'SearchValue'] = $routeParams['id__int'];
scope[list.iterator + 'SearchFieldLabel'] = 'Job ID';
}
+ if ($routeParams['status']) {
+ scope[list.iterator + 'SearchField'] = 'status';
+ scope[list.iterator + 'SelectShow'] = true;
+ scope[list.iterator + 'SearchSelectOpts'] = list.fields['status'].searchOptions;
+ scope[list.iterator + 'SearchFieldLabel'] = list.fields['status'].label.replace(/\
/g,' ');
+ for (var opt in list.fields['status'].searchOptions) {
+ if (list.fields['status'].searchOptions[opt].value == $routeParams['status']) {
+ scope[list.iterator + 'SearchSelectValue'] = list.fields['status'].searchOptions[opt];
+ break;
+ }
+ }
+ }
scope.search(list.iterator);
@@ -162,7 +174,8 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest,
JobsListCtrl.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobList',
'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
- 'ProcessErrors','GetBasePath', 'LookUpInit', 'SubmitJob', 'FormatDate', 'Refresh', 'JobStatusToolTip'
+ 'ProcessErrors','GetBasePath', 'LookUpInit', 'SubmitJob', 'FormatDate', 'Refresh', 'JobStatusToolTip',
+ 'Empty'
];
diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js
index 6262ff3e78..75d13fdd64 100644
--- a/awx/ui/static/js/forms/Credentials.js
+++ b/awx/ui/static/js/forms/Credentials.js
@@ -186,6 +186,26 @@ angular.module('CredentialFormDefinition', [])
awPassMatch: true,
associated: 'ssh_key_unlock'
},
+ "scm_key_unlock": {
+ label: 'Key Password',
+ type: 'password',
+ ngShow: "kind.value == 'scm'",
+ addRequired: false,
+ editRequired: false,
+ ngChange: "clearPWConfirm('scm_key_unlock_confirm')",
+ associated: 'scm_key_unlock_confirm',
+ ask: false,
+ clear: true
+ },
+ "scm_key_unlock_confirm": {
+ label: 'Confirm Key Password',
+ type: 'password',
+ ngShow: "kind.value == 'scm'",
+ addRequired: false,
+ editRequired: false,
+ awPassMatch: true,
+ associated: 'scm_key_unlock'
+ },
"sudo_username": {
label: 'Sudo Username',
type: 'text',
diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js
index 040be96606..fe180554c5 100644
--- a/awx/ui/static/js/helpers/Groups.js
+++ b/awx/ui/static/js/helpers/Groups.js
@@ -79,6 +79,49 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
}
}])
+ .factory('ViewUpdateStatus', [ 'Rest', 'ProcessErrors', 'GetBasePath', 'ShowUpdateStatus', 'Alert',
+ function(Rest, ProcessErrors, GetBasePath, ShowUpdateStatus, Alert) {
+ return function(params) {
+
+ var scope = params.scope;
+ var id = params.group_id;
+ var found = false;
+ var group;
+ for (var i=0; i < scope.groups.length; i++) {
+ if (scope.groups[i].id == id) {
+ found = true;
+ group = scope.groups[i];
+ }
+ }
+ if (found) {
+ if (group.summary_fields.inventory_source.source == "" || group.summary_fields.inventory_source.source == null) {
+ Alert('Missing Configuration', 'The selected group is not configured for inventory updates. ' +
+ 'You must first edit the group, provide Source settings, and then run an update.', 'alert-info');
+ }
+ else if (group.summary_fields.inventory_source.status == "" || group.summary_fields.inventory_source.status == null ||
+ group.summary_fields.inventory_source.status == "never updated") {
+ Alert('No Status Available', 'The inventory update process has not run for the selected group. Start the process by ' +
+ 'clicking the Update button.', 'alert-info');
+ }
+ else {
+ Rest.setUrl(group.related.inventory_source);
+ Rest.get()
+ .success( function(data, status, headers, config) {
+ var url = (data.related.current_update) ? data.related.current_update : data.related.last_update;
+ ShowUpdateStatus({ group_name: data.summary_fields.group.name,
+ last_update: url });
+ })
+ .error( function(data, status, headers, config) {
+ ProcessErrors(scope, data, status, form,
+ { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source +
+ ' POST returned status: ' + status });
+ });
+ }
+ }
+
+ }
+ }])
+
.factory('HostsStatusMsg', [ function() {
return function(params) {
var active_failures = params.active_failures;
@@ -278,11 +321,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
}])
.factory('InventoryStatus', [ '$rootScope', '$routeParams', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventorySummary',
- 'GenerateList', 'ClearScope', 'SearchInit', 'PaginateInit', 'Refresh', 'InventoryUpdate', 'GroupsEdit', 'ShowUpdateStatus', 'HelpDialog',
- 'InventorySummaryHelp', 'BuildTree', 'ClickNode', 'HostsStatusMsg', 'UpdateStatusMsg',
- function($rootScope, $routeParams, Rest, Alert, ProcessErrors, GetBasePath, FormatDate, InventorySummary, GenerateList, ClearScope, SearchInit,
- PaginateInit, Refresh, InventoryUpdate, GroupsEdit, ShowUpdateStatus, HelpDialog, InventorySummaryHelp, BuildTree, ClickNode,
- HostsStatusMsg, UpdateStatusMsg) {
+ 'GenerateList', 'ClearScope', 'SearchInit', 'PaginateInit', 'Refresh', 'InventoryUpdate', 'GroupsEdit', 'HelpDialog',
+ 'InventorySummaryHelp', 'BuildTree', 'ClickNode', 'HostsStatusMsg', 'UpdateStatusMsg', 'ViewUpdateStatus',
+ function($rootScope, $routeParams, Rest, Alert, ProcessErrors, GetBasePath, FormatDate, InventorySummary, GenerateList, ClearScope,
+ SearchInit, PaginateInit, Refresh, InventoryUpdate, GroupsEdit, HelpDialog, InventorySummaryHelp, BuildTree, ClickNode,
+ HostsStatusMsg, UpdateStatusMsg, ViewUpdateStatus) {
return function(params) {
//Build a summary of a given inventory
@@ -373,41 +416,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
HelpDialog({ defn: InventorySummaryHelp });
}
- scope.viewUpdateStatus = function(id) {
- var found = false;
- var group;
- for (var i=0; i < scope.groups.length; i++) {
- if (scope.groups[i].id == id) {
- found = true;
- group = scope.groups[i];
- }
- }
- if (found) {
- if (group.summary_fields.inventory_source.source == "" || group.summary_fields.inventory_source.source == null) {
- Alert('Missing Configuration', 'The selected group is not configured for inventory updates. ' +
- 'You must first edit the group, provide Source settings, and then run an update.', 'alert-info');
- }
- else if (group.summary_fields.inventory_source.status == "" || group.summary_fields.inventory_source.status == null ||
- group.summary_fields.inventory_source.status == "never updated") {
- Alert('No Status Available', 'The inventory update process has not run for the selected group. Start the process by ' +
- 'clicking the Update button.', 'alert-info');
- }
- else {
- Rest.setUrl(group.related.inventory_source);
- Rest.get()
- .success( function(data, status, headers, config) {
- var url = (data.related.current_update) ? data.related.current_update : data.related.last_update;
- ShowUpdateStatus({ group_name: data.summary_fields.group.name,
- last_update: url });
- })
- .error( function(data, status, headers, config) {
- ProcessErrors(scope, data, status, form,
- { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source +
- ' POST returned status: ' + status });
- });
- }
- }
- }
+ scope.viewUpdateStatus = function(group_id) { ViewUpdateStatus({ scope: scope, group_id: group_id }) };
// Click on group name
scope.GroupsEdit = function(group_id) {
diff --git a/awx/ui/static/js/lists/HomeGroups.js b/awx/ui/static/js/lists/HomeGroups.js
new file mode 100644
index 0000000000..18eb328cad
--- /dev/null
+++ b/awx/ui/static/js/lists/HomeGroups.js
@@ -0,0 +1,119 @@
+/*********************************************
+ * Copyright (c) 2013 AnsibleWorks, Inc.
+ *
+ * HomeGroups.js
+ *
+ * List view object for Group data model. Used
+ * on the home tab.
+ *
+ */
+angular.module('HomeGroupListDefinition', [])
+ .value(
+ 'HomeGroupList', {
+
+ name: 'groups',
+ iterator: 'group',
+ editTitle: 'Groups',
+ index: true,
+ hover: true,
+
+ fields: {
+ name: {
+ key: true,
+ label: 'Group',
+ ngClick: "\{\{ 'GroupsEdit(' + group.id + ')' \}\}",
+ columnClass: 'col-lg-3 col-md3 col-sm-2',
+ linkTo: "\{\{ '/#/inventories/' + group.inventory + '/groups/?name=' + group.name \}\}"
+ },
+ inventory_name: {
+ label: 'Inventory',
+ sourceModel: 'inventory',
+ sourceField: 'name',
+ columnClass: 'col-lg-3 col-md3 col-sm-2',
+ linkTo: "\{\{ '/#/inventories/' + group.inventory \}\}"
+ },
+ failed_hosts: {
+ label: 'Failed Hosts',
+ ngHref: "\{\{ group.failed_hosts_link \}\}",
+ badgeIcon: "\{\{ 'icon-failures-' + group.failed_hosts_class \}\}",
+ badgeNgHref: "\{\{ group.failed_hosts_link \}\}",
+ badgePlacement: 'left',
+ badgeToolTip: "\{\{ group.failed_hosts_tip \}\}",
+ badgeTipPlacement: 'top',
+ awToolTip: "\{\{ group.failed_hosts_tip \}\}",
+ dataPlacement: "top",
+ searchable: false,
+ excludeModal: true,
+ sortField: "hosts_with_active_failures"
+ },
+ status: {
+ label: 'Status',
+ ngClick: "viewUpdateStatus(\{\{ group.id \}\})",
+ searchType: 'select',
+ badgeIcon: "\{\{ 'icon-cloud-' + group.status_badge_class \}\}",
+ badgeToolTip: "\{\{ group.status_badge_tooltip \}\}",
+ awToolTip: "\{\{ group.status_badge_tooltip \}\}",
+ dataPlacement: 'top',
+ badgeTipPlacement: 'top',
+ badgePlacement: 'left',
+ searchOptions: [
+ { name: "failed", value: "failed" },
+ { name: "never", value: "never updated" },
+ { name: "n/a", value: "none" },
+ { name: "successful", value: "successful" },
+ { name: "updating", value: "updating" }],
+ sourceModel: 'inventory_source',
+ sourceField: 'status'
+ },
+ last_updated: {
+ label: 'Last
Updated',
+ sourceModel: 'inventory_source',
+ sourceField: 'last_updated',
+ searchable: false,
+ nosort: false
+ },
+ source: {
+ label: 'Source',
+ searchType: 'select',
+ searchOptions: [
+ { name: "ec2", value: "ec2" },
+ { name: "none", value: "" },
+ { name: "rackspace", value: "rackspace" }],
+ sourceModel: 'inventory_source',
+ sourceField: 'source',
+ searchOnly: true
+ },
+ has_external_source: {
+ label: 'Has external source?',
+ searchType: 'in',
+ searchValue: 'ec2,rackspace',
+ searchOnly: true,
+ sourceModel: 'inventory_source',
+ sourceField: 'source'
+ },
+ has_active_failures: {
+ label: 'Has failed hosts?',
+ searchSingleValue: true,
+ searchType: 'boolean',
+ searchValue: 'true',
+ searchOnly: true
+ },
+ last_update_failed: {
+ label: 'Update failed?',
+ searchType: 'select',
+ searchSingleValue: true,
+ searchValue: 'failed',
+ searchOnly: true,
+ sourceModel: 'inventory_source',
+ sourceField: 'status'
+ }
+ },
+
+ actions: {
+
+ },
+
+ fieldActions: {
+
+ }
+ });
diff --git a/awx/ui/static/js/lists/InventorySummary.js b/awx/ui/static/js/lists/InventorySummary.js
index 046b1dcad4..fc680356f1 100644
--- a/awx/ui/static/js/lists/InventorySummary.js
+++ b/awx/ui/static/js/lists/InventorySummary.js
@@ -133,7 +133,8 @@ angular.module('InventorySummaryDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
- ngClick: "refresh()"
+ ngClick: "refresh()",
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/JobEvents.js b/awx/ui/static/js/lists/JobEvents.js
index 9f8bdd8093..0e9acaffc0 100644
--- a/awx/ui/static/js/lists/JobEvents.js
+++ b/awx/ui/static/js/lists/JobEvents.js
@@ -92,7 +92,8 @@ angular.module('JobEventsListDefinition', [])
ngShow: "job_status == 'pending' || job_status == 'waiting' || job_status == 'running'",
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
- ngClick: "refresh()"
+ ngClick: "refresh()",
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/JobHosts.js b/awx/ui/static/js/lists/JobHosts.js
index 8784c14c18..4cfd73c2b7 100644
--- a/awx/ui/static/js/lists/JobHosts.js
+++ b/awx/ui/static/js/lists/JobHosts.js
@@ -126,7 +126,8 @@ angular.module('JobHostDefinition', [])
ngShow: "host_id == null && (job_status == 'pending' || job_status == 'waiting' || job_status == 'running')",
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
- ngClick: "refresh()"
+ ngClick: "refresh()",
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/Jobs.js b/awx/ui/static/js/lists/Jobs.js
index 5ebd0e1a61..80ef8b7245 100644
--- a/awx/ui/static/js/lists/Jobs.js
+++ b/awx/ui/static/js/lists/Jobs.js
@@ -81,7 +81,8 @@ angular.module('JobsListDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
- ngClick: "refresh()"
+ ngClick: "refresh()",
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/Projects.js b/awx/ui/static/js/lists/Projects.js
index 517e4936eb..64d37e6c91 100644
--- a/awx/ui/static/js/lists/Projects.js
+++ b/awx/ui/static/js/lists/Projects.js
@@ -78,7 +78,8 @@ angular.module('ProjectsListDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
- ngClick: "refresh()"
+ ngClick: "refresh()",
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js
new file mode 100644
index 0000000000..ff17ad1cb4
--- /dev/null
+++ b/awx/ui/static/js/lists/Streams.js
@@ -0,0 +1,62 @@
+/*********************************************
+ * Copyright (c) 2013 AnsibleWorks, Inc.
+ *
+ * Streams.js
+ * List view object for activity stream data model.
+ *
+ *
+ */
+angular.module('StreamListDefinition', [])
+ .value(
+ 'StreamList', {
+
+ name: 'activities',
+ iterator: 'activity',
+ editTitle: 'Activity Stream',
+ selectInstructions: '',
+ index: false,
+ hover: true,
+ "class": "table-condensed",
+
+ fields: {
+ event_time: {
+ key: true,
+ label: 'When'
+ },
+ user: {
+ label: 'Who',
+ sourceModel: 'user',
+ sourceField: 'username'
+ },
+ operation: {
+ label: 'Operation'
+ },
+ description: {
+ label: 'Description'
+ }
+ },
+
+ actions: {
+ refresh: {
+ dataPlacement: 'top',
+ icon: "icon-refresh",
+ mode: 'all',
+ 'class': 'btn-xs btn-primary',
+ awToolTip: "Refresh the page",
+ ngClick: "refreshStream()",
+ iconSize: 'large'
+ },
+ close: {
+ dataPlacement: 'top',
+ icon: "icon-arrow-left",
+ mode: 'all',
+ 'class': 'btn-xs btn-primary',
+ awToolTip: "Close Activity Stream view",
+ ngClick: "closeStream()",
+ iconSize: 'large'
+ }
+ },
+
+ fieldActions: {
+ }
+ });
\ No newline at end of file
diff --git a/awx/ui/static/js/widgets/JobStatus.js b/awx/ui/static/js/widgets/JobStatus.js
index e8d015ba3d..574ada224d 100644
--- a/awx/ui/static/js/widgets/JobStatus.js
+++ b/awx/ui/static/js/widgets/JobStatus.js
@@ -15,7 +15,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities'])
var scope = $rootScope.$new();
var jobCount, jobFails, inventoryCount, inventoryFails, groupCount, groupFails, hostCount, hostFails;
var counts = 0;
- var expectedCounts = 8;
+ var expectedCounts = 6;
var target = params.target;
if (scope.removeCountReceived) {
@@ -25,17 +25,21 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities'])
var rowcount = 0;
- function makeRow(label, count, fail) {
+ function makeRow(params) {
var html = '';
+ var label = params.label;
+ var link = params.link;
+ var fail_link = params.fail_link;
+ var count = params.count;
+ var fail = params.fail;
html += "