First iteration of Activity Stream. Added Home/Groups page. Increased icon size for icon-only buttons. Dashboard jobs widget- group and job links now work. Closed AC-621, AC-618.

This commit is contained in:
Chris Houseknecht 2013-11-08 17:58:19 +00:00
parent 2c4d583f3e
commit 5a3977495a
25 changed files with 1204 additions and 101 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -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 == '') {

View File

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

View File

@ -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'];
'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(/\<br\>/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'
];

View File

@ -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(/\<br\>/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'
];

View File

@ -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',

View File

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

View File

@ -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<br>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: {
}
});

View File

@ -133,7 +133,8 @@ angular.module('InventorySummaryDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
ngClick: "refresh()"
ngClick: "refresh()",
iconSize: 'large'
}
},

View File

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

View File

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

View File

@ -81,7 +81,8 @@ angular.module('JobsListDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
ngClick: "refresh()"
ngClick: "refresh()",
iconSize: 'large'
}
},

View File

@ -78,7 +78,8 @@ angular.module('ProjectsListDefinition', [])
mode: 'all',
'class': 'btn-xs btn-primary',
awToolTip: "Refresh the page",
ngClick: "refresh()"
ngClick: "refresh()",
iconSize: 'large'
}
},

View File

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

View File

@ -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 += "<tr>\n";
html += "<td><a href=\"/#/" + label.toLowerCase() + "\"";
html += (label == 'Hosts' || label == 'Groups') ? " class=\"pad-left-sm\" " : "";
html += "<td><a href=\"" + link + "\"";
html += ">" + label + "</a></td>\n";
html += "<td class=\"failed-column text-right\">";
html += (fail > 0) ? "<a href=\"/blah/blah\">" + fail + "</a>" : "";
html += "<a href=\"" + fail_link + "\">" + fail + "</a>";
html += "</td>\n";
html += "<td class=\"text-right\">"
html += (count > 0) ? "<a href=\"/blah/blah\">" + count + "</a>" : "";
html += "<a href=\"" + link + "\">" + count + "</a>";
html += "</td></tr>\n";
return html;
}
@ -57,19 +61,33 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities'])
html += "<tbody>\n";
if (jobCount > 0) {
html += makeRow('Jobs', jobCount, jobFails);
rowcount++;
}
if (inventoryCount > 0) {
html += makeRow('Inventories', inventoryCount, inventoryFails);
html += makeRow({
label: 'Jobs',
link: '/#/jobs',
count: jobCount,
fail: jobFails,
fail_link: '/#/jobs/?status=failed'
});
rowcount++;
}
if (groupCount > 0) {
html += makeRow('Groups', groupCount, groupFails);
html += makeRow({
label: 'Groups',
link: '/#/home/groups',
count: groupCount,
fail: groupFails,
fail_link: '/#/home/groups/?status=failed'
});
rowcount++;
}
if (hostCount > 0) {
html += makeRow('Hosts', hostCount, hostFails);
html += makeRow({
label: 'Hosts',
link: '#/home/hosts',
count: hostCount,
fail: hostFails,
fail_link: '/#/home/hosts/?status=failed'
});
rowcount++;
}
@ -114,7 +132,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities'])
{ hdr: 'Error!', msg: 'Failed to get ' + url + '. GET status: ' + status });
});
url = GetBasePath('inventory') + '?page=1';
/*url = GetBasePath('inventory') + '?page=1';
Rest.setUrl(url);
Rest.get()
.success( function(data, status, headers, config) {
@ -136,7 +154,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities'])
.error( function(data, status, headers, config) {
ProcessErrors(scope, data, status, null,
{ hdr: 'Error!', msg: 'Failed to get ' + url + '. GET status: ' + status });
});
});*/
url = GetBasePath('groups') + '?page=1';
Rest.setUrl(url);

View File

@ -0,0 +1,102 @@
/*********************************************
* Copyright (c) 2013 AnsibleWorks, Inc.
*
* Stream.js
*
* Activity stream widget that can be called from anywhere
*
*/
angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginateHelper',
'RefreshHelper', 'ListGenerator', 'StreamWidget'])
.factory('ShowStream', [ function() {
return function() {
// Slide in the Stream widget
var stream = $('#stream-container');
stream.css({
position: 'absolute',
left: 0,
top: 0,
width: '100%',
'min-height': '100%',
'background-color': '#FFF'
});
stream.show('slide', {'direction': 'left'}, {'duration': 500, 'queue': false });
}
}])
.factory('HideStream', [ 'ClearScope', function(ClearScope) {
return function() {
// Remove the stream widget
var stream = $('#stream-container');
stream.hide('slide', {'direction': 'left'}, {'duration': 500, 'queue': false });
// Completely destroy the container so we don't experience random flashes of it later.
// There was some sort weirdness with the tab 'show' causing the stream to slide in when
// a tab was clicked, after the stream had been hidden. Seemed like timing- wait long enough
// before clicking a tab, and it would not happen.
setTimeout( function() {
stream.detach();
stream.empty();
stream.unbind();
}, 500);
}
}])
.factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit',
'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream',
function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList,
FormatDate, ShowStream, HideStream) {
return function(params) {
var list = StreamList;
var defaultUrl = $basePath + 'html/event_log.html/';
var view = GenerateList;
// Push the current page onto browser histor. If user clicks back button, restore current page without
// stream widget
// window.history.pushState({}, "AnsibleWorks AWX", $location.path());
// Add a container for the stream widget
$('#tab-content-container').append('<div id="stream-container"><div id=\"stream-content\"></div></div><!-- Stream widget -->');
// Generate the list
var scope = view.inject(list, {
mode: 'edit',
id: 'stream-content',
breadCrumbs: true,
searchSize: 'col-lg-4'
});
scope.closeStream = function() {
HideStream();
}
scope.refreshStream = function() {
scope['activities'].splice(10,10);
//scope.search(list.iterator);
}
if (scope.removePostRefresh) {
scope.removePostRefresh();
}
scope.removePostRefresh = scope.$on('PostRefresh', function() {
for (var i=0; i < scope['activities'].length; i++) {
// Convert event_time date to local time zone
cDate = new Date(scope['activities'][i].event_time);
scope['activities'][i].event_time = FormatDate(cDate);
// Display username
scope['activities'][i].user = scope.activities[i].summary_fields.user.username;
}
ShowStream();
});
// Initialize search and paginate pieces and load data
SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl });
PaginateInit({ scope: scope, list: list, url: defaultUrl });
scope.search(list.iterator);
}
}]);

View File

@ -487,10 +487,10 @@ legend {
margin: 10px 0 0 0;
}
.page-size {
height: 25px;
font-size: 10.5px;
line-height: normal;
select.page-size {
width: 65px;
height: 24px;
font-size: 10px;
}
.page-size-label {
@ -642,7 +642,7 @@ input[type="checkbox"].checkbox-no-label {
text-align: right;
button {
margin-left: 8px;
margin-left: 4px;
}
}
@ -1374,6 +1374,31 @@ tr td button i {
}
/* Activity Stream Widget */
#stream-container {
display: none;
}
#stream-content {
border: 1px solid @grey;
border-radius: 8px;
padding: 8px;
}
/*
.activity-btn {
padding-left: 2px;
padding-right: 2px;
padding-bottom: 2px;
img {
width: 16px;
height: 16px;
}
}
*/
/* Large desktop */
@media (min-width: 1200px) {

View File

@ -109,14 +109,10 @@ angular.module('Utilities',['RestServices', 'Utilities'])
}
Alert(defaultMsg.hdr, msg);
}
else if (status == 401 && data.detail && data.detail == 'Token is expired') {
else if ( (status == 401 && data.detail && data.detail == 'Token is expired') ||
(status == 401 && data.detail && data.detail == 'Invalid token') ) {
$rootScope.sessionTimer.expireSession();
window.location = '/#/login'; //resetting location so that we drop search params
}
else if (status == 401 && data.detail && data.detail == 'Invalid token') {
// should this condition be treated as an expired session?? Yes, for now.
$rootScope.sessionTimer.expireSession();
window.location = '/#/login'; //resetting location so that we drop search params
$location.url('/login');
}
else if (data.non_field_errors) {
Alert('Error!', data.non_field_errors);

View File

@ -138,6 +138,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
html += (btn.ngShow) ? Attr(btn, 'ngShow') : "";
html += (btn.ngHide) ? Attr(btn, 'ngHide') : "";
html += " >";
html += (btn['img']) ? "<img src=\"" + $basePath + "img/" + btn.img + "\" style=\"width: 12px; height: 12px;\" >" : "";
html += (btn['icon']) ? Attr(btn,'icon') : "";
html += (btn['awRefresh'] && !btn['icon']) ? "<i class=\"icon-refresh\"></i> " : "";
html += (btn.label) ? " " + btn.label : "";
@ -595,7 +596,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
html += "<select ng-model=\"" + iterator + "PageSize\" ng-change=\"changePageSize('" +
set + "'," + "'" + iterator + "')\" ";
html += "id=\"page_size_select\" ";
html += "class=\"page-size\">\n";
html += "class=\"page-size input-sm form-control\">\n";
html += "<option value=\"10\" selected>10</option>\n";
html += "<option value=\"20\" selected>20</option>\n";
html += "<option value=\"40\">40</option>\n";

View File

@ -55,6 +55,13 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
}
this.setList(list);
element.html(this.build(options)); // Inject the html
if (options.prepend) { // Add any extra HTML passed in options
element.prepend(options.prepend);
}
if (options.append) {
element.append(options.prepend);
}
this.scope = element.scope(); // Set scope specific to the element we're compiling, avoids circular reference
// From here use 'scope' to manipulate the form, as the form is not in '$scope'
$compile(element)(this.scope);
@ -170,7 +177,7 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
html += "<strong>Hint: </strong>" + list.editInstructions + "\n";
html += "</div>\n";
}
if (options.mode != 'lookup' && (list.well == undefined || list.well == true)) {
html += "<div class=\"well\">\n";
}
@ -186,7 +193,10 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
}
*/
if (options.mode == 'summary') {
if (options.searchSize) {
html += SearchWidget({ iterator: list.iterator, template: list, mini: true , size: options.searchSize });
}
else if (options.mode == 'summary') {
html += SearchWidget({ iterator: list.iterator, template: list, mini: true , size: 'col-lg-6' });
}
else if (options.mode == 'lookup' || options.id != undefined) {
@ -200,8 +210,13 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
//actions
var base = $location.path().replace(/^\//,'').split('/')[0];
html += "<div class=\"";
if (options.mode == 'summary') {
html += "<div class=\"";
if (options.searchSize) {
// User supplied searchSize, calc the remaining
var size = parseInt(options.searchSize.replace(/([A-Z]|[a-z]|\-)/g,''));
html += 'col-lg-' + (11 - size);
}
else if (options.mode == 'summary') {
html += 'col-lg-5';
}
else if (options.id != undefined) {

View File

@ -1,8 +1,9 @@
<div class="tab-pane" id="home">
<div id="refresh-row" class="row">
<div class="col-lg-12">
<div class="refresh-grp pull-right">
<button type="button" class="btn btn-primary btn-xs refresh-btn" ng-click="refreshCnt = 10; refresh()" id="refresh_btn" aw-tool-tip="Refresh page" data-placement="top" data-original-title="" title=""><i class="icon-refresh"></i></button>
<div class="list-actions pull-right">
<button type="button" class="btn btn-primary btn-xs refresh-btn" ng-click="refresh()" id="refresh_btn" aw-tool-tip="Refresh page" data-placement="top"><i class="icon-refresh icon-large"></i></button>
<button type="button" class="btn btn-primary btn-xs activity-btn" ng-click="showActivity()" id="activity_btn" aw-tool-tip="View activity stream" data-placement="top"><i class="icon-comments-alt icon-large"></i></button>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
<div class="tab-pane" id="home">
<div id="htmlTemplate"></div>
</div>

View File

@ -90,6 +90,8 @@
<script src="{{ STATIC_URL }}js/lists/JobEvents.js"></script>
<script src="{{ STATIC_URL }}js/lists/JobHosts.js"></script>
<script src="{{ STATIC_URL }}js/lists/Permissions.js"></script>
<script src="{{ STATIC_URL }}js/lists/Streams.js"></script>
<script src="{{ STATIC_URL }}js/lists/HomeGroups.js"></script>
<script src="{{ STATIC_URL }}js/helpers/refresh-related.js"></script>
<script src="{{ STATIC_URL }}js/helpers/related-paginate.js"></script>
<script src="{{ STATIC_URL }}js/helpers/related-search.js"></script>
@ -117,13 +119,14 @@
<script src="{{ STATIC_URL }}js/widgets/InventorySyncStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/SCMSyncStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/ObjectCount.js"></script>
<script src="{{ STATIC_URL }}js/widgets/Stream.js"></script>
<script src="{{ STATIC_URL }}js/help/InventorySummary.js"></script>
<script src="{{ STATIC_URL }}js/help/InventoryHosts.js"></script>
<script src="{{ STATIC_URL }}lib/less/less-1.4.1.min.js"></script>
{% endif %}
</head>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top main-menu" role="navigation">
@ -149,7 +152,7 @@
</div>
</div><!-- navbar -->
<div class="container main-container">
<div class="container main-container" id="main">
<div class="row">
<div class="col-lg-12">
@ -165,7 +168,7 @@
<li><a href="#jobs" id="main_jobs_tab" data-toggle="tab">Jobs</a></li>
</ul>
<div class="tab-content">
<div class="tab-content" id="tab-content-container">
<div ng-view id="main-view"></div>
</div>
</div>