mirror of
https://github.com/ansible/awx.git
synced 2026-01-22 06:58:06 -03:30
AC-414 Making red/green bubbles consistent throughout the application. Changes made to Inventory and jobs pages. Tooltip text now dynamic. Improved deep linking, especially on status fields.
This commit is contained in:
parent
6bf8f9cd98
commit
b28fe1254e
@ -70,7 +70,8 @@ angular.module('ansible', [
|
||||
'LicenseFormDefinition',
|
||||
'License',
|
||||
'HostGroupsFormDefinition',
|
||||
'ObjectCountWidget'
|
||||
'ObjectCountWidget',
|
||||
'JobsHelper'
|
||||
])
|
||||
.config(['$routeProvider', function($routeProvider) {
|
||||
$routeProvider.
|
||||
|
||||
@ -43,15 +43,15 @@ function InventoryHosts ($scope, $rootScope, $compile, $location, $log, $routePa
|
||||
LoadSearchTree({ scope: scope, inventory_id: scope['inventory_id'] });
|
||||
|
||||
// Add the selected flag to the hosts set.
|
||||
if (scope.relatedHostsRemove) {
|
||||
scope.relatedHostsRemove();
|
||||
}
|
||||
scope.relatedHostsRemove = scope.$on('relatedhosts', function() {
|
||||
scope.toggleAllFlag = false;
|
||||
for (var i=0; i < scope.hosts.length; i++) {
|
||||
scope.hosts[i].selected = 0;
|
||||
}
|
||||
});
|
||||
//if (scope.relatedHostsRemove) {
|
||||
// scope.relatedHostsRemove();
|
||||
//}
|
||||
//scope.relatedHostsRemove = scope.$on('relatedhosts', function() {
|
||||
// scope.toggleAllFlag = false;
|
||||
// for (var i=0; i < scope.hosts.length; i++) {
|
||||
// scope.hosts[i].selected = 0;
|
||||
// }
|
||||
// });
|
||||
|
||||
scope.filterHosts = function() {
|
||||
HostsReload({ scope: scope, inventory_id: scope['inventory_id'], group_id: scope['group_id'] });
|
||||
|
||||
@ -30,65 +30,79 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res
|
||||
scope.search(list.iterator);
|
||||
|
||||
LoadBreadCrumbs();
|
||||
|
||||
|
||||
if (scope.projectsPostRefresh) {
|
||||
scope.projectsPostRefresh();
|
||||
}
|
||||
scope.projectsPostRefresh = scope.$on('PostRefresh', function() {
|
||||
for (var i=0; i < scope.inventories.length; i++) {
|
||||
if (scope.inventories[i].hosts_with_active_failures > 0) {
|
||||
scope.inventories[i].active_failures_params = "/?has_active_failures=true";
|
||||
}
|
||||
//if (scope.inventories[i].hosts_with_active_failures < 99) {
|
||||
// scope.inventories[i].hosts_with_active_failures = ('00' + scope.inventories[i].hosts_with_active_failures).substr(-2);
|
||||
//}
|
||||
}
|
||||
});
|
||||
|
||||
scope.addInventory = function() {
|
||||
$location.path($location.path() + '/add');
|
||||
}
|
||||
$location.path($location.path() + '/add');
|
||||
}
|
||||
|
||||
scope.editInventory = function(id) {
|
||||
$location.path($location.path() + '/' + id);
|
||||
}
|
||||
$location.path($location.path() + '/' + id);
|
||||
}
|
||||
|
||||
scope.deleteInventory = function(id, name) {
|
||||
|
||||
var action = function() {
|
||||
var url = defaultUrl + id + '/';
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.destroy()
|
||||
.success( function(data, status, headers, config) {
|
||||
scope.search(list.iterator);
|
||||
Wait('stop');
|
||||
})
|
||||
.error( function(data, status, headers, config) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null,
|
||||
{ hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
var action = function() {
|
||||
var url = defaultUrl + id + '/';
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.destroy()
|
||||
.success( function(data, status, headers, config) {
|
||||
scope.search(list.iterator);
|
||||
Wait('stop');
|
||||
})
|
||||
.error( function(data, status, headers, config) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null,
|
||||
{ hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({ hdr: 'Delete',
|
||||
body: 'Are you sure you want to delete ' + name + '?',
|
||||
action: action
|
||||
});
|
||||
}
|
||||
Prompt({ hdr: 'Delete',
|
||||
body: 'Are you sure you want to delete ' + name + '?',
|
||||
action: action
|
||||
});
|
||||
}
|
||||
|
||||
scope.lookupOrganization = function(organization_id) {
|
||||
Rest.setUrl(GetBasePath('organizations') + organization_id + '/');
|
||||
Rest.get()
|
||||
.success( function(data, status, headers, config) {
|
||||
return data.name;
|
||||
});
|
||||
}
|
||||
scope.lookupOrganization = function(organization_id) {
|
||||
Rest.setUrl(GetBasePath('organizations') + organization_id + '/');
|
||||
Rest.get()
|
||||
.success( function(data, status, headers, config) {
|
||||
return data.name;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status
|
||||
scope.viewJobs = function(id) {
|
||||
$location.url('/jobs/?inventory__int=' + id);
|
||||
}
|
||||
// Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status
|
||||
scope.viewJobs = function(id) {
|
||||
$location.url('/jobs/?inventory__int=' + id);
|
||||
}
|
||||
|
||||
scope.viewFailedJobs = function(id) {
|
||||
$location.url('/jobs/?inventory__int=' + id + '&status=failed');
|
||||
}
|
||||
scope.viewFailedJobs = function(id) {
|
||||
$location.url('/jobs/?inventory__int=' + id + '&status=failed');
|
||||
}
|
||||
|
||||
scope.editHosts = function(id) {
|
||||
$location.url('/inventories/' + id + '/hosts');
|
||||
}
|
||||
scope.editHosts = function(id) {
|
||||
$location.url('/inventories/' + id + '/hosts');
|
||||
}
|
||||
|
||||
scope.editGroups = function(id) {
|
||||
$location.url('/inventories/' + id + '/groups');
|
||||
}
|
||||
scope.editGroups = function(id) {
|
||||
$location.url('/inventories/' + id + '/groups');
|
||||
}
|
||||
}
|
||||
|
||||
InventoriesList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'InventoryList', 'GenerateList',
|
||||
|
||||
@ -131,12 +131,39 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest,
|
||||
set[i]['spaces'] = set[i].event_level * 24;
|
||||
if (scope.jobevents[i].failed) {
|
||||
scope.jobevents[i].status = 'error';
|
||||
if (i == set.length - 1) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A failure occurred durring one or more playbook tasks.";
|
||||
}
|
||||
else if (set[i].event_level < 3) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A failure occurred within the children of this event.";
|
||||
}
|
||||
else {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A failure occurred. Click to view details";
|
||||
}
|
||||
}
|
||||
else if (scope.jobevents[i].changed) {
|
||||
scope.jobevents[i].status = 'changed';
|
||||
if (i == set.length - 1) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A change was completed durring one or more playbook tasks.";
|
||||
}
|
||||
else if (set[i].event_level < 3) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A change was completed by one or more children of this event.";
|
||||
}
|
||||
else {
|
||||
scope.jobevents[i].statusBadgeToolTip = "A change was completed. Click to view details";
|
||||
}
|
||||
}
|
||||
else {
|
||||
scope.jobevents[i].status = 'success';
|
||||
if (i == set.length - 1) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "All playbook tasks completed successfully.";
|
||||
}
|
||||
else if (set[i].event_level < 3) {
|
||||
scope.jobevents[i].statusBadgeToolTip = "All the children of this event completed successfully.";
|
||||
}
|
||||
else {
|
||||
scope.jobevents[i].statusBadgeToolTip = "No errors occurred. Click to view details";
|
||||
}
|
||||
}
|
||||
cDate = new Date(set[i].created);
|
||||
set[i].created = FormatDate(cDate);
|
||||
@ -147,6 +174,8 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest,
|
||||
Rest.get()
|
||||
.success( function(data, status, headers, config) {
|
||||
scope.job_status = data.status;
|
||||
scope.job_name = data.summary_fields.job_template.name;
|
||||
LoadBreadCrumbs({ path: '/jobs/' + scope.job_id, title: scope.job_name });
|
||||
if (!(data.status == 'pending' || data.status == 'waiting' || data.status == 'running')) {
|
||||
if ($rootScope.timer) {
|
||||
clearInterval($rootScope.timer);
|
||||
@ -181,8 +210,6 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest,
|
||||
children: children
|
||||
});
|
||||
}
|
||||
|
||||
LoadBreadCrumbs();
|
||||
|
||||
scope.viewJobEvent = function(id) {
|
||||
EventView({ event_id: id });
|
||||
|
||||
@ -12,13 +12,14 @@
|
||||
|
||||
function JobHostSummaryList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobHostList,
|
||||
GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller,
|
||||
ClearScope, ProcessErrors, GetBasePath, Refresh)
|
||||
ClearScope, ProcessErrors, GetBasePath, Refresh, JobStatusToolTip)
|
||||
{
|
||||
ClearScope('htmlTemplate');
|
||||
var list = JobHostList;
|
||||
var base = $location.path().replace(/^\//,'').split('/')[0];
|
||||
var defaultUrl = GetBasePath(base) + $routeParams.id + '/job_host_summaries/';
|
||||
|
||||
var inventory_id;
|
||||
|
||||
// When viewing all summaries for a particular host, show job ID, otherwise row ID.
|
||||
if (base == 'hosts') {
|
||||
list.index = false;
|
||||
@ -42,46 +43,71 @@ function JobHostSummaryList ($scope, $rootScope, $location, $log, $routeParams,
|
||||
scope.host_id = null;
|
||||
}
|
||||
|
||||
if (scope.RemoveSetHostLink) {
|
||||
scope.RemoveSetHostLink();
|
||||
}
|
||||
scope.RemoveSetHostLink = scope.$on('setHostLink', function(e, inventory_id) {
|
||||
for (var i=0; i < scope.jobhosts.length; i++) {
|
||||
scope.jobhosts[i].hostLinkTo = '/#/inventories/' + inventory_id + '/hosts/?name=' +
|
||||
escape(scope.jobhosts[i].summary_fields.host.name);
|
||||
}
|
||||
});
|
||||
|
||||
// After a refresh, populate any needed summary field values on each row
|
||||
if (scope.PostRefreshRemove) {
|
||||
scope.PostRefreshRemove();
|
||||
}
|
||||
scope.PostRefershRemove = scope.$on('PostRefresh', function() {
|
||||
scope.PostRefreshRemove = scope.$on('PostRefresh', function() {
|
||||
|
||||
// Set status, tooltips, badget icons, etc.
|
||||
for( var i=0; i < scope.jobhosts.length; i++) {
|
||||
scope.jobhosts[i].host_name = scope.jobhosts[i].summary_fields.host.name;
|
||||
scope.jobhosts[i].status = (scope.jobhosts[i].failed) ? 'error' : 'success';
|
||||
scope.jobhosts[i].status = (scope.jobhosts[i].failed) ? 'failed' : 'success';
|
||||
scope.jobhosts[i].statusBadgeToolTip = JobStatusToolTip(scope.jobhosts[i].status) +
|
||||
" Click to view details.";
|
||||
scope.jobhosts[i].statusLinkTo = '/#/jobs/' + scope.jobhosts[i].job + '/job_events/?host=' +
|
||||
escape(scope.jobhosts[i].summary_fields.host.name);
|
||||
}
|
||||
|
||||
if (scope.job_id !== null && scope.job_id !== undefined && scope.job_id !== '') {
|
||||
// need job_status so we can show/hide refresh button
|
||||
Rest.setUrl(GetBasePath('jobs') + scope.job_id);
|
||||
Rest.get()
|
||||
.success( function(data, status, headers, config) {
|
||||
scope.job_status = data.status;
|
||||
scope.$emit('setHostLink', data.inventory);
|
||||
if (!(data.status == 'pending' || data.status == 'waiting' || data.status == 'running')) {
|
||||
if ($rootScope.timer) {
|
||||
clearInterval($rootScope.timer);
|
||||
}
|
||||
}
|
||||
})
|
||||
.error( function(data, status, headers, config) {
|
||||
.error( function(data, status, headers, config) {
|
||||
ProcessErrors(scope, data, status, null,
|
||||
{ hdr: 'Error!', msg: 'Failed to get job status for job: ' + scope.job_id + '. GET status: ' + status });
|
||||
});
|
||||
}
|
||||
if (base == 'hosts' && $routeParams['host_name']) {
|
||||
// Make the host name appear in breadcrumbs
|
||||
LoadBreadCrumbs({ path: '/hosts/' + scope['host_id'], title: $routeParams['host_name'] });
|
||||
}
|
||||
else {
|
||||
LoadBreadCrumbs();
|
||||
}
|
||||
});
|
||||
|
||||
SearchInit({ scope: scope, set: 'jobhosts', list: list, url: defaultUrl });
|
||||
PaginateInit({ scope: scope, list: list, url: defaultUrl });
|
||||
|
||||
// Called from Inventories tab, host failed events link:
|
||||
if ($routeParams.host) {
|
||||
if ($routeParams['host_name']) {
|
||||
scope[list.iterator + 'SearchField'] = 'host';
|
||||
scope[list.iterator + 'SearchValue'] = $routeParams.host;
|
||||
scope[list.iterator + 'SearchValue'] = $routeParams['host_name'];
|
||||
scope[list.iterator + 'SearchFieldLabel'] = list.fields['host'].label;
|
||||
}
|
||||
|
||||
scope.search(list.iterator);
|
||||
LoadBreadCrumbs();
|
||||
|
||||
|
||||
scope.showEvents = function(host_name, last_job) {
|
||||
// When click on !Failed Events link, redirect to latest job/job_events for the host
|
||||
@ -113,5 +139,5 @@ function JobHostSummaryList ($scope, $rootScope, $location, $log, $routeParams,
|
||||
|
||||
JobHostSummaryList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobHostList',
|
||||
'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
|
||||
'ProcessErrors', 'GetBasePath', 'Refresh'
|
||||
'ProcessErrors', 'GetBasePath', 'Refresh', 'JobStatusToolTip'
|
||||
];
|
||||
|
||||
@ -27,6 +27,14 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re
|
||||
|
||||
SearchInit({ scope: scope, set: 'job_templates', list: list, url: defaultUrl });
|
||||
PaginateInit({ scope: scope, list: list, url: defaultUrl });
|
||||
|
||||
// Called from Inventories tab, host failed events link:
|
||||
if ($routeParams['name']) {
|
||||
scope[list.iterator + 'SearchField'] = 'name';
|
||||
scope[list.iterator + 'SearchValue'] = $routeParams['name'];
|
||||
scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label;
|
||||
}
|
||||
|
||||
scope.search(list.iterator);
|
||||
|
||||
LoadBreadCrumbs();
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
|
||||
function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobList,
|
||||
GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller,
|
||||
ClearScope, ProcessErrors, GetBasePath, LookUpInit, SubmitJob, FormatDate, Refresh)
|
||||
ClearScope, ProcessErrors, GetBasePath, LookUpInit, SubmitJob, FormatDate, Refresh,
|
||||
JobStatusToolTip)
|
||||
{
|
||||
ClearScope('htmlTemplate');
|
||||
var list = JobList;
|
||||
@ -41,6 +42,13 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest,
|
||||
scope[list.name][i].created = FormatDate(cDate);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i=0; i < scope.jobs.length; i++) {
|
||||
scope.jobs[i].statusBadgeToolTip = JobStatusToolTip(scope.jobs[i].status) +
|
||||
" Click to view status details.";
|
||||
scope.jobs[i].statusLinkTo = '/#/jobs/' + scope.jobs[i].id;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -157,14 +165,14 @@ 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'
|
||||
'ProcessErrors','GetBasePath', 'LookUpInit', 'SubmitJob', 'FormatDate', 'Refresh', 'JobStatusToolTip'
|
||||
];
|
||||
|
||||
|
||||
function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, JobForm,
|
||||
GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit,
|
||||
RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, CredentialList,
|
||||
ProjectList, LookUpInit, PromptPasswords, GetBasePath, md5Setup, FormatDate)
|
||||
ProjectList, LookUpInit, PromptPasswords, GetBasePath, md5Setup, FormatDate, JobStatusToolTip)
|
||||
{
|
||||
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
|
||||
//scope.
|
||||
@ -334,7 +342,9 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scope.statusToolTip = JobStatusToolTip(data.status);
|
||||
|
||||
$('form[name="jobs_form"] input[type="text"], form[name="jobs_form"] jobs_form textarea').attr('readonly','readonly');
|
||||
$('form[name="jobs_form"] select').prop('disabled', 'disabled');
|
||||
$('form[name="jobs_form"] .lookup-btn').prop('disabled', 'disabled');
|
||||
@ -503,5 +513,6 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams,
|
||||
JobsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'JobForm',
|
||||
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit',
|
||||
'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'InventoryList', 'CredentialList',
|
||||
'ProjectList', 'LookUpInit', 'PromptPasswords', 'GetBasePath', 'md5Setup', 'FormatDate'
|
||||
'ProjectList', 'LookUpInit', 'PromptPasswords', 'GetBasePath', 'md5Setup', 'FormatDate',
|
||||
'JobStatusToolTip'
|
||||
];
|
||||
|
||||
@ -18,13 +18,29 @@ angular.module('InventoryHostsFormDefinition', [])
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Host Name',
|
||||
ngClick: "editHost(\{\{ host.id \}\}, '\{\{ host.name \}\}')",
|
||||
label: 'Name',
|
||||
ngClick: "editHost(\{\{ host.id \}\}, '\{\{ host.name \}\}')"
|
||||
//columnClass: 'col-lg-3'
|
||||
},
|
||||
active_failures: {
|
||||
label: 'Current<br>Job Status?',
|
||||
ngHref: "\{\{ host.activeFailuresLink \}\}",
|
||||
awToolTip: "\{\{ host.badgeToolTip \}\}",
|
||||
dataPlacement: 'bottom',
|
||||
badgeNgHref: '\{\{ host.activeFailuresLink \}\}',
|
||||
badgeIcon: "\{\{ 'icon-failures-' + host.has_active_failures \}\}",
|
||||
badgeToolTip: 'Indicates if host has active failures',
|
||||
badgePlacement: 'left',
|
||||
badgeToolTip: "\{\{ host.badgeToolTip \}\}",
|
||||
badgeTipPlacement: 'bottom',
|
||||
columnClass: 'col-lg-3'
|
||||
searchable: false,
|
||||
nosort: true
|
||||
},
|
||||
has_active_failures: {
|
||||
label: 'Current job failed?',
|
||||
searchSingleValue: true,
|
||||
searchType: 'boolean',
|
||||
searchValue: 'true',
|
||||
searchOnly: true
|
||||
},
|
||||
groups: {
|
||||
label: 'Groups',
|
||||
@ -32,7 +48,7 @@ angular.module('InventoryHostsFormDefinition', [])
|
||||
sourceModel: 'groups',
|
||||
sourceField: 'name',
|
||||
nosort: true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -270,7 +270,7 @@ angular.module('JobFormDefinition', [])
|
||||
status: {
|
||||
label: 'Job Status',
|
||||
type: 'custom',
|
||||
control: '<div class="job-detail-status job-\{\{ status \}\}"><i class="icon-circle"></i> \{\{ status \}\}</div>',
|
||||
control: '<div class=\"job-detail-status\"><i class=\"icon-job-\{\{ status \}\}\"></i> \{\{ status \}\}</div>',
|
||||
readonly: true
|
||||
},
|
||||
created: {
|
||||
|
||||
@ -27,7 +27,7 @@ angular.module('EventsHelper', ['RestServices', 'Utilities', 'JobEventDataDefini
|
||||
fields: {
|
||||
status: {
|
||||
labelClass: 'job-\{\{ status \}\}',
|
||||
icon: 'icon-circle',
|
||||
icon: 'icon-job-\{\{ status \}\}',
|
||||
type: 'custom',
|
||||
section: 'Event',
|
||||
control: '<div class=\"job-event-status job-\{\{ status \}\}\">\{\{ status \}\}</div>'
|
||||
|
||||
@ -147,26 +147,35 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
|
||||
for (var i=0; i < scope.groups.length; i++) {
|
||||
var last_update = (scope.groups[i].last_updated == null) ? '' : FormatDate(new Date(scope.groups[i].last_updated));
|
||||
var source = 'Manual';
|
||||
var stat;
|
||||
var stat_class;
|
||||
var stat, stat_class, status_tip;
|
||||
|
||||
stat = scope.groups[i].status;
|
||||
stat_class = stat;
|
||||
switch (scope.groups[i].status) {
|
||||
case 'never updated':
|
||||
stat = 'never';
|
||||
stat_class = 'never';
|
||||
status_tip = 'Inventory update has not been performed. Click Update button to start it now.';
|
||||
break;
|
||||
case 'none':
|
||||
stat = 'n/a';
|
||||
stat_class = 'na';
|
||||
status_tip = 'Not configured for inventory update.';
|
||||
break;
|
||||
case 'failed':
|
||||
status_tip = 'Inventory update completed with errors. Click to view process output.';
|
||||
break;
|
||||
case 'successful':
|
||||
status_tip = 'Inventory update completed with no errors. Click to view process output.';
|
||||
break;
|
||||
case 'updating':
|
||||
status_tip = 'Inventory update process running now.';
|
||||
break;
|
||||
default:
|
||||
stat = scope.groups[i].status;
|
||||
stat_class = stat;
|
||||
}
|
||||
|
||||
switch (scope.groups[i].source) {
|
||||
case 'file':
|
||||
source = 'File';
|
||||
source = 'Local Script';
|
||||
break;
|
||||
case 'ec2':
|
||||
source = 'Amazon EC2';
|
||||
@ -175,10 +184,21 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
|
||||
source = 'Rackspace';
|
||||
break;
|
||||
}
|
||||
|
||||
if (scope.groups[i].summary_fields.group.hosts_with_active_failures > 0) {
|
||||
scope.groups[i].active_failures_params = "/?has_active_failures=true";
|
||||
}
|
||||
else {
|
||||
scope.groups[i].active_failures_params = '';
|
||||
}
|
||||
|
||||
scope.groups[i].hosts_with_active_failures = scope.groups[i].summary_fields.group.hosts_with_active_failures;
|
||||
scope.groups[i].has_active_failures = scope.groups[i].summary_fields.group.has_active_failures;
|
||||
scope.groups[i].status = stat;
|
||||
scope.groups[i].source = source;
|
||||
scope.groups[i].last_updated = last_update;
|
||||
scope.groups[i].status_class = stat_class;
|
||||
scope.groups[i].status_badge_class = stat_class;
|
||||
scope.groups[i].status_badge_tooltip = status_tip;
|
||||
}
|
||||
});
|
||||
|
||||
@ -688,6 +708,36 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
|
||||
});
|
||||
}
|
||||
}
|
||||
// Start the update process
|
||||
scope.updateGroup = function() {
|
||||
if (scope[source] == "" || scope.groups[i].source == null) {
|
||||
Alert('Missing Configuration', 'The group is not configured for updates. You must provide Source settings before running the update ' +
|
||||
'process.');
|
||||
}
|
||||
else if (scope[status] == 'updating') {
|
||||
Alert('Update in Progress', 'The inventory update process is currently running for this group <em>' +
|
||||
scope.groups[i].summary_fields.group.name + '</em>. Under the Groupmonitor the status.', 'alert-info');
|
||||
}
|
||||
else {
|
||||
if (scope['source'] == 'Amazon EC2') {
|
||||
scope['sourceUsernameLabel'] = 'Access Key ID';
|
||||
scope['sourcePasswordLabel'] = 'Secret Access Key';
|
||||
scope['sourcePasswordConfirmLabel'] = 'Confirm Secret Access Key';
|
||||
}
|
||||
else {
|
||||
scope['sourceUsernameLabel'] = 'Username';
|
||||
scope['sourcePasswordLabel'] = 'Password';
|
||||
scope['sourcePasswordConfirmLabel'] = 'Confirm Password';
|
||||
}
|
||||
InventoryUpdate({
|
||||
scope: scope,
|
||||
group_id: id,
|
||||
url: scope.groups[i].related.update,
|
||||
group_name: scope.groups[i].summary_fields.group.name,
|
||||
group_source: scope.groups[i].source
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel
|
||||
scope.formReset = function() {
|
||||
@ -697,6 +747,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', '
|
||||
}
|
||||
scope.parseType = 'yaml';
|
||||
}
|
||||
|
||||
}
|
||||
}])
|
||||
|
||||
|
||||
@ -408,8 +408,8 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
}])
|
||||
|
||||
|
||||
.factory('HostsReload', ['SearchInit', 'PaginateInit', 'InventoryHostsForm', 'GetBasePath', 'Wait',
|
||||
function(SearchInit, PaginateInit, InventoryHostsForm, GetBasePath, Wait) {
|
||||
.factory('HostsReload', ['$routeParams', 'SearchInit', 'PaginateInit', 'InventoryHostsForm', 'GetBasePath', 'Wait',
|
||||
function($routeParams, SearchInit, PaginateInit, InventoryHostsForm, GetBasePath, Wait) {
|
||||
return function(params) {
|
||||
// Rerfresh the Hosts view on right side of page
|
||||
|
||||
@ -424,15 +424,13 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
var url = (group_id !== null && group_id !== undefined) ? GetBasePath('groups') + group_id + '/all_hosts/' :
|
||||
GetBasePath('inventory') + params.inventory_id + '/hosts/';
|
||||
|
||||
if (scope.hostFailureFilter) {
|
||||
url += '?has_active_failures=true';
|
||||
}
|
||||
|
||||
// Set the groups value in each element of hosts array
|
||||
if (scope.removePostRefresh) {
|
||||
scope.removePostRefresh();
|
||||
}
|
||||
scope.removePostRefresh = scope.$on('PostRefresh', function() {
|
||||
|
||||
// Add a list of groups to each host
|
||||
var groups, descr, found, list;
|
||||
for (var i=0; i < scope.hosts.length; i++) {
|
||||
groups = scope.hosts[i].summary_fields.groups;
|
||||
@ -444,14 +442,54 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
}
|
||||
scope.hosts[i].groups = scope.hosts[i].groups.replace(/\, $/,'');
|
||||
}
|
||||
|
||||
// Add the value displayed in Job Status column
|
||||
for (var i=0; i < scope.hosts.length; i++) {
|
||||
scope.hosts[i].activeFailuresLink = '/#/hosts/' + scope.hosts[i].id + '/job_host_summaries/?inventory=' + scope['inventory_id'] +
|
||||
'&host_name=' + escape(scope.hosts[i].name);
|
||||
if (scope.hosts[i].has_active_failures == true) {
|
||||
scope.hosts[i].badgeToolTip = 'Most recent job failed. Click to view jobs.';
|
||||
scope.hosts[i].active_failures = 'failed';
|
||||
}
|
||||
else if (scope.hosts[i].has_active_failures == false && scope.hosts[i].last_job == null) {
|
||||
scope.hosts[i].has_active_failures = 'none';
|
||||
scope.hosts[i].badgeToolTip = "No job data available.";
|
||||
scope.hosts[i].active_failures = 'n/a';
|
||||
}
|
||||
else if (scope.hosts[i].has_active_failures == false && scope.hosts[i].last_job !== null) {
|
||||
scope.hosts[i].badgeToolTip = "Most recent job successful. Click to view jobs.";
|
||||
scope.hosts[i].active_failures = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
if (postAction) {
|
||||
postAction();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
SearchInit({ scope: scope, set: 'hosts', list: InventoryHostsForm, url: url });
|
||||
PaginateInit({ scope: scope, list: InventoryHostsForm, url: url });
|
||||
scope.search('host');
|
||||
|
||||
if ($routeParams['has_active_failures']) {
|
||||
//scope.resetSearch(InventoryHostsForm.iterator);
|
||||
scope[InventoryHostsForm.iterator + 'InputDisable'] = true;
|
||||
scope[InventoryHostsForm.iterator + 'SearchValue'] = $routeParams['has_active_failures'];
|
||||
scope[InventoryHostsForm.iterator + 'SearchField'] = 'has_active_failures';
|
||||
scope[InventoryHostsForm.iterator + 'SearchFieldLabel'] = InventoryHostsForm.fields['has_active_failures'].label;
|
||||
scope[InventoryHostsForm.iterator + 'SearchSelectValue'] = ($routeParams['has_active_failures'] == 'true') ? { value: 1 } : { value: 0 };
|
||||
}
|
||||
|
||||
if ($routeParams['name']) {
|
||||
console.log('here!');
|
||||
scope[InventoryHostsForm.iterator + 'InputDisable'] = false;
|
||||
scope[InventoryHostsForm.iterator + 'SearchValue'] = $routeParams['name'];
|
||||
scope[InventoryHostsForm.iterator + 'SearchField'] = 'name';
|
||||
scope[InventoryHostsForm.iterator + 'SearchFieldLabel'] = InventoryHostsForm.fields['name'].label;
|
||||
scope[InventoryHostsForm.iterator + 'SearchSelectValue'] = null;
|
||||
}
|
||||
|
||||
scope.search(InventoryHostsForm.iterator);
|
||||
|
||||
if (!params.scope.$$phase) {
|
||||
params.scope.$digest();
|
||||
@ -467,6 +505,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
var scope = params.scope;
|
||||
var inventory_id = params.inventory_id;
|
||||
var html = '';
|
||||
var toolTip = 'Hosts have failed jobs?';
|
||||
|
||||
function buildHTML(tree_data) {
|
||||
var sorted = SortNodes(tree_data);
|
||||
@ -480,7 +519,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
"data-group-id=\"" + sorted[i].id + "\" " +
|
||||
"><a href=\"\" class=\"expand\"><i class=\"icon-caret-down\"></i></a> " +
|
||||
"<i class=\"field-badge icon-failures-" + sorted[i].has_active_failures + "\" " +
|
||||
"aw-tool-tip=\"Indicates if group contains hosts with active failures\" data-placement=\"bottom\"></i> " +
|
||||
"aw-tool-tip=\"" + toolTip + "\" data-placement=\"bottom\"></i> " +
|
||||
"<a href=\"\" class=\"activate\">" + sorted[i].name + "</a> ";
|
||||
if (sorted[i].children.length > 0) {
|
||||
buildHTML(sorted[i].children);
|
||||
@ -651,7 +690,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H
|
||||
"data-name=\"" + data.name + "\" " +
|
||||
"><a href=\"\" class=\"expand\"><i class=\"icon-caret-down\"></i></a> " +
|
||||
"<i class=\"field-badge icon-failures-" + data.has_active_failures + "\"" +
|
||||
"aw-tool-tip=\"Indicates if group contains hosts with active failures\" data-placement=\"bottom\"></i> " +
|
||||
"aw-tool-tip=\"" + toolTip + "\" data-placement=\"bottom\"></i> " +
|
||||
"<a href=\"\" class=\"activate active\">" + data.name + "</a>";
|
||||
scope.$emit('buildAllGroups', data.name, data.related.tree, data.related.groups);
|
||||
scope.$emit('refreshHost', null, 'All Hosts');
|
||||
|
||||
40
awx/ui/static/js/helpers/Jobs.js
Normal file
40
awx/ui/static/js/helpers/Jobs.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
*
|
||||
* JobsHelper
|
||||
*
|
||||
* Routines shared by job related controllers
|
||||
*
|
||||
*/
|
||||
|
||||
angular.module('JobsHelper', [ ])
|
||||
.factory('JobStatusToolTip', [ function() {
|
||||
return function(status) {
|
||||
var toolTip;
|
||||
switch (status) {
|
||||
case 'successful':
|
||||
case 'success':
|
||||
toolTip = 'There were no failed tasks.';
|
||||
break;
|
||||
case 'failed':
|
||||
toolTip = 'Some tasks encountered errors.';
|
||||
break;
|
||||
case 'canceled':
|
||||
toolTip = 'Stopped by user request.';
|
||||
break;
|
||||
case 'new':
|
||||
toolTip = 'In queue, waiting on task manager.';
|
||||
break;
|
||||
case 'waiting':
|
||||
toolTip = 'SCM Update or Inventory Update is executing.';
|
||||
break;
|
||||
case 'pending':
|
||||
toolTip = 'Not in queue, waiting on task manager.';
|
||||
break;
|
||||
case 'running':
|
||||
toolTip = 'Playbook tasks executing.';
|
||||
break;
|
||||
}
|
||||
return toolTip;
|
||||
}
|
||||
}]);
|
||||
@ -166,7 +166,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi
|
||||
|
||||
$(tree_id).bind('move_node.jstree', function(e, data) {
|
||||
// When user drags-n-drops a node, update the API
|
||||
|
||||
Wait('start');
|
||||
|
||||
var node, target, url, parent, inv_id, variables;
|
||||
@ -273,12 +272,11 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi
|
||||
}
|
||||
});
|
||||
|
||||
// When user clicks on a group
|
||||
$(tree_id).bind("select_node.jstree", function(e, data){
|
||||
scope.$emit('NodeSelect', data.inst.get_json()[0]);
|
||||
});
|
||||
// When user clicks on a group
|
||||
$(tree_id).bind("select_node.jstree", function(e, data){
|
||||
scope.$emit('NodeSelect', data.inst.get_json()[0]);
|
||||
});
|
||||
|
||||
|
||||
Wait('start');
|
||||
LoadTreeData(params);
|
||||
|
||||
|
||||
@ -83,19 +83,57 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper'])
|
||||
scope[iterator + 'SelectShow'] = false;
|
||||
scope[iterator + 'HideSearchType'] = false;
|
||||
scope[iterator + 'InputHide'] = false;
|
||||
scope[iterator + 'InputDisable'] = false;
|
||||
scope[iterator + 'SearchType'] = 'icontains';
|
||||
|
||||
if (list.fields[fld].searchType && list.fields[fld].searchType == 'gtzero') {
|
||||
scope[iterator + "InputHide"] = true;
|
||||
}
|
||||
if (list.fields[fld].searchType && (list.fields[fld].searchType == 'boolean'
|
||||
else if (list.fields[fld].searchSingleValue){
|
||||
// Query a specific attribute for one specific value
|
||||
// searchSingleValue: true
|
||||
// searchType: 'boolean|int|etc.'
|
||||
// searchValue: < value to match for boolean use 'true'|'false' >
|
||||
scope[iterator + "SearchType"] = list.fields[fld].searchType;
|
||||
scope[iterator + 'InputDisable'] = true;
|
||||
scope[iterator + "SearchValue"] = list.fields[fld].searchValue;
|
||||
// For boolean type, SearchValue must be an object
|
||||
if (list.fields[fld].searchType == 'boolean' && list.fields[fld].searchValue == 'true') {
|
||||
scope[iterator + "SearchSelectValue"] = { value: 1 };
|
||||
}
|
||||
else if (list.fields[fld].searchType == 'boolean' && list.fields[fld].searchValue == 'false') {
|
||||
scope[iterator + "SearchSelectValue"] = { value: 0 };
|
||||
}
|
||||
else if (list.fields[fld].searchType == 'boolean') {
|
||||
scope[iterator + "SearchSelectValue"] = { value: list.fields[fld].searchValue };
|
||||
}
|
||||
}
|
||||
else if (list.fields[fld].searchType && (list.fields[fld].searchType == 'boolean'
|
||||
|| list.fields[fld].searchType == 'select')) {
|
||||
scope[iterator + 'SelectShow'] = true;
|
||||
scope[iterator + 'SearchSelectOpts'] = list.fields[fld].searchOptions;
|
||||
}
|
||||
if (list.fields[fld].searchType && list.fields[fld].searchType == 'int') {
|
||||
else if (list.fields[fld].searchType && list.fields[fld].searchType == 'int') {
|
||||
scope[iterator + 'HideSearchType'] = true;
|
||||
}
|
||||
scope.search(iterator);
|
||||
}
|
||||
|
||||
scope.resetSearch = function(iterator) {
|
||||
// Respdond to click of reset button
|
||||
scope[iterator + "SearchValue"] = '';
|
||||
scope[iterator + "SearchSelectValue"] = '';
|
||||
scope[iterator + 'SelectShow'] = false;
|
||||
scope[iterator + 'HideSearchType'] = false;
|
||||
scope[iterator + 'InputHide'] = false;
|
||||
scope[iterator + 'InputDisable'] = false;
|
||||
for (fld in list.fields) {
|
||||
if (list.fields[fld].searchable == undefined || list.fields[fld].searchable == true) {
|
||||
scope[iterator + 'SearchFieldLabel'] = list.fields[fld].label;
|
||||
scope[iterator + 'SearchField'] = fld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
scope.search(iterator);
|
||||
}
|
||||
|
||||
|
||||
@ -21,21 +21,37 @@ angular.module('InventoriesListDefinition', [])
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
badgeIcon: "\{\{ 'icon-failures-' + inventory.has_active_failures \}\}",
|
||||
badgePlacement: 'left',
|
||||
badgeToolTip: 'Indicates if inventory contains hosts with active failures',
|
||||
badgeTipPlacement: 'bottom'
|
||||
label: 'Name'
|
||||
},
|
||||
description: {
|
||||
label: 'Description'
|
||||
label: 'Description',
|
||||
link: true
|
||||
},
|
||||
hosts_with_active_failures: {
|
||||
label: 'Hosts with<br>Failed Job?',
|
||||
ngHref: '/#/inventories/{{ inventory.id }}/hosts{{ inventory.active_failures_params }}',
|
||||
type: 'badgeCount',
|
||||
"class": "{{ 'failures-' + inventory.has_active_failures }}",
|
||||
//badgeIcon: "\{\{ 'icon-failures-' + inventory.has_active_failures \}\}",
|
||||
//badgePlacement: 'left',
|
||||
awToolTip: '# of hosts with failed jobs. Click to view hosts.',
|
||||
dataPlacement: 'bottom',
|
||||
searchable: false
|
||||
},
|
||||
organization: {
|
||||
label: 'Organization',
|
||||
ngBind: 'inventory.summary_fields.organization.name',
|
||||
linkTo: '/organizations/{{ inventory.organization }}',
|
||||
sourceModel: 'organization',
|
||||
sourceField: 'name',
|
||||
excludeModal: true
|
||||
},
|
||||
has_active_failures: {
|
||||
label: 'Hosts with failed jobs?',
|
||||
searchSingleValue: true,
|
||||
searchType: 'boolean',
|
||||
searchValue: 'true',
|
||||
searchOnly: true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -25,27 +25,35 @@ angular.module('InventorySummaryDefinition', [])
|
||||
noLink: true,
|
||||
ngBind: "group.summary_fields.group.name",
|
||||
sourceModel: 'group',
|
||||
sourceField: 'name',
|
||||
badges: [
|
||||
{ //Active Failures
|
||||
icon: "\{\{ 'icon-failures-' + group.summary_fields.group.has_active_failures \}\}",
|
||||
toolTip: 'Indicates if inventory contains hosts with active failures',
|
||||
toolTipPlacement: 'bottom'
|
||||
},
|
||||
{ //Cloud Status
|
||||
icon: "\{\{ 'icon-cloud-' + group.status_class \}\}",
|
||||
toolTip: 'Indicates status of inventory update process',
|
||||
toolTipPlacement: 'bottom'
|
||||
}]
|
||||
sourceField: 'name'
|
||||
},
|
||||
failures: {
|
||||
label: 'Active<br>Failures',
|
||||
ngBind: "group.summary_fields.group.hosts_with_active_failures",
|
||||
sourceModel: 'group',
|
||||
sourceField: 'hosts_with_active_failures',
|
||||
searchField: 'group__has_active_failures',
|
||||
searchType: 'boolean',
|
||||
searchOptions: [{ name: "yes", value: 1 }, { name: "no", value: 0 }]
|
||||
hosts_with_active_failures: {
|
||||
label: 'Hosts with<br>Job Failures?',
|
||||
ngHref: '/#/inventories/{{ inventory_id }}/hosts{{ group.active_failures_params }}',
|
||||
type: 'badgeCount',
|
||||
"class": "{{ 'failures-' + group.has_active_failures }}",
|
||||
awToolTip: '# of hosts with job failures. Click to view hosts.',
|
||||
dataPlacement: 'bottom',
|
||||
searchable: false,
|
||||
nosort: true
|
||||
},
|
||||
status: {
|
||||
label: 'Update<br>Status',
|
||||
searchType: 'select',
|
||||
badgeIcon: 'icon-cloud',
|
||||
badgeToolTip: "\{\{ group.status_badge_tooltip \}\}",
|
||||
badgePlacement: 'left',
|
||||
badgeClass: "\{\{ 'icon-cloud-' + group.status_badge_class \}\}",
|
||||
searchOptions: [
|
||||
{ name: "failed", value: "failed" },
|
||||
{ name: "never", value: "never updated" },
|
||||
{ name: "n/a", value: "none" },
|
||||
{ name: "successful", value: "successful" },
|
||||
{ name: "updating", value: "updating" }]
|
||||
},
|
||||
last_updated: {
|
||||
label: 'Last<br>Updated',
|
||||
searchable: false
|
||||
},
|
||||
source: {
|
||||
label: 'Source',
|
||||
@ -56,25 +64,20 @@ angular.module('InventorySummaryDefinition', [])
|
||||
{ name: "Manual", value: "" },
|
||||
{ name: "Rackspace", value: "rackspace" }]
|
||||
},
|
||||
last_updated: {
|
||||
label: 'Last<br>Updated',
|
||||
searchable: false
|
||||
},
|
||||
status: {
|
||||
label: 'Update<br>Status',
|
||||
searchType: 'select',
|
||||
searchOptions: [
|
||||
{ name: "failed", value: "failed" },
|
||||
{ name: "never", value: "never updated" },
|
||||
{ name: "n/a", value: "none" },
|
||||
{ name: "successful", value: "successful" },
|
||||
{ name: "updating", value: "updating" }]
|
||||
has_active_failures: {
|
||||
label: 'Hosts have job failures?',
|
||||
searchSingleValue: true,
|
||||
searchType: 'boolean',
|
||||
searchValue: 'true',
|
||||
searchOnly: true,
|
||||
sourceModel: 'group',
|
||||
sourceField: 'has_active_failures'
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
refresh: {
|
||||
awRefresh: true,
|
||||
awRefresh: false,
|
||||
mode: 'all'
|
||||
},
|
||||
help: {
|
||||
|
||||
@ -48,14 +48,20 @@ angular.module('JobEventsListDefinition', [])
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
icon: 'icon-circle',
|
||||
showValue: true,
|
||||
"class": 'job-\{\{ jobevent.status \}\}',
|
||||
searchField: 'failed',
|
||||
searchType: 'boolean',
|
||||
searchOptions: [{ name: "success", value: 0 }, { name: "error", value: 1 }],
|
||||
nosort: true,
|
||||
searchable: false
|
||||
searchable: false,
|
||||
ngClick: "viewJobEvent(\{\{ jobevent.id \}\})",
|
||||
awToolTip: "\{\{ jobevent.statusBadgeToolTip \}\}",
|
||||
dataPlacement: 'top',
|
||||
badgeIcon: 'icon-job-\{\{ jobevent.status \}\}',
|
||||
badgePlacement: 'left',
|
||||
badgeToolTip: "\{\{ jobevent.statusBadgeToolTip \}\}",
|
||||
badgeTipPlacement: 'top',
|
||||
badgeNgClick: "viewJobEvent(\{\{ jobevent.id \}\})"
|
||||
},
|
||||
event_display: {
|
||||
label: 'Event',
|
||||
|
||||
@ -38,7 +38,7 @@ angular.module('JobHostDefinition', [])
|
||||
},
|
||||
|
||||
fields: {
|
||||
id: {
|
||||
job: {
|
||||
label: 'Job ID',
|
||||
ngClick: "showJob(\{\{ jobhost.job \}\})",
|
||||
columnShow: 'host_id !== null',
|
||||
@ -51,12 +51,18 @@ angular.module('JobHostDefinition', [])
|
||||
sourceModel: 'host',
|
||||
sourceField: 'name',
|
||||
ngBind: 'jobhost.host_name',
|
||||
ngClick:"showEvents('\{\{ jobhost.summary_fields.host.name \}\}','\{\{ jobhost.related.job \}\}')"
|
||||
ngHref: "\{\{ jobhost.hostLinkTo \}\}"
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
icon: 'icon-circle',
|
||||
"class": 'job-\{\{ jobhost.status \}\}',
|
||||
badgeNgHref: "\{\{ jobhost.statusLinkTo \}\}",
|
||||
badgeIcon: 'icon-job-\{\{ jobhost.status \}\}',
|
||||
badgePlacement: 'left',
|
||||
badgeToolTip: "\{\{ jobhost.statusBadgeToolTip \}\}",
|
||||
badgeTipPlacement: 'top',
|
||||
ngHref: "\{\{ jobhost.statusLinkTo \}\}",
|
||||
awToolTip: "\{\{ jobhost.statusBadgeToolTip \}\}",
|
||||
dataPlacement: 'top',
|
||||
searchField: 'failed',
|
||||
searchType: 'boolean',
|
||||
searchOptions: [{ name: "success", value: 0 }, { name: "error", value: 1 }]
|
||||
|
||||
@ -37,15 +37,15 @@ angular.module('JobsListDefinition', [])
|
||||
job_template: {
|
||||
label: 'Job Template',
|
||||
ngBind: 'job.summary_fields.job_template.name',
|
||||
link: true,
|
||||
ngHref: "\{\{ '/#/job_templates/?name=' + job.summary_fields.job_template.name \}\}",
|
||||
sourceModel: 'job_template',
|
||||
sourceField: 'name'
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
icon: 'icon-circle',
|
||||
"class": 'job-\{\{ job.status \}\}',
|
||||
searchType: 'select',
|
||||
linkTo: "\{\{ job.statusLinkTo \}\}",
|
||||
searchOptions: [
|
||||
{ name: "new", value: "new" },
|
||||
{ name: "waiting", value: "waiting" },
|
||||
@ -54,7 +54,14 @@ angular.module('JobsListDefinition', [])
|
||||
{ name: "successful", value: "successful" },
|
||||
{ name: "error", value: "error" },
|
||||
{ name: "failed", value: "failed" },
|
||||
{ name: "canceled", value: "canceled" } ]
|
||||
{ name: "canceled", value: "canceled" } ],
|
||||
badgeIcon: 'icon-job-\{\{ job.status \}\}',
|
||||
badgePlacement: 'left',
|
||||
badgeToolTip: "\{\{ job.statusBadgeToolTip \}\}",
|
||||
badgeTipPlacement: 'top',
|
||||
badgeNgHref: "\{\{ job.statusLinkTo \}\}",
|
||||
awToolTip: "\{\{ job.statusBadgeToolTip \}\}",
|
||||
dataPlacement: 'top'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -9,11 +9,12 @@
|
||||
|
||||
@black: #171717;
|
||||
@warning: #FF9900;
|
||||
@red: #FF0000;
|
||||
@red: #da4f49;;
|
||||
@green: #5bb75b;
|
||||
@blue: #1778c3; /* logo blue */
|
||||
@blue-link: #0088cc;
|
||||
@grey: #A9A9A9;
|
||||
@green: #5bb75b;
|
||||
|
||||
html {
|
||||
background-color: @black;
|
||||
@ -32,6 +33,7 @@ body {
|
||||
.no-bullets { list-style: none; }
|
||||
.capitalize { text-transform: capitalize; }
|
||||
.grey-txt { color: @grey; }
|
||||
.text-center { text-align: center !important; }
|
||||
|
||||
.success-badge {
|
||||
color: #ffffff;
|
||||
@ -92,10 +94,6 @@ body {
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
@ -607,8 +605,6 @@ select.field-mini-height {
|
||||
|
||||
/* Jobs pages */
|
||||
|
||||
.job-error,
|
||||
.job-failed,
|
||||
.license-expired,
|
||||
.license-invalid,
|
||||
.icon-failures-true,
|
||||
@ -633,33 +629,87 @@ select.field-mini-height {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.job-new,
|
||||
|
||||
input[type="text"].job-new,
|
||||
.job-canceled,
|
||||
input[type="text"].job-canceled {
|
||||
color: #778899;
|
||||
}
|
||||
|
||||
.job-pending,
|
||||
.job-running,
|
||||
.job-success,
|
||||
.job-successful,
|
||||
.job-waiting,
|
||||
.icon-failures-false,
|
||||
.license-valid,
|
||||
input[type="text"].job-success,
|
||||
input[type="text"].job-successful {
|
||||
color: #5bb75b;
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.icon-job-pending:before,
|
||||
.icon-job-running:before,
|
||||
.icon-job-success:before,
|
||||
.icon-job-successful:before,
|
||||
.icon-job-waiting:before,
|
||||
.icon-job-new:before,
|
||||
.icon-job-canceled:before,
|
||||
.icon-job-changed:before {
|
||||
content: "\f111";
|
||||
}
|
||||
|
||||
.icon-job-error:before,
|
||||
.icon-job-failed:before {
|
||||
content: "\f06a";
|
||||
}
|
||||
|
||||
.icon-job-pending,
|
||||
.icon-job-running,
|
||||
.icon-job-success,
|
||||
.icon-job-successful,
|
||||
.icon-job-waiting,
|
||||
.icon-job-new {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.icon-job-canceled {
|
||||
color: @grey;
|
||||
}
|
||||
|
||||
.icon-job-changed {
|
||||
color: @warning;
|
||||
}
|
||||
|
||||
.icon-job-error,
|
||||
.icon-job-failed {
|
||||
color: @red;
|
||||
}
|
||||
|
||||
.icon-failures-none {
|
||||
color: @grey;
|
||||
}
|
||||
|
||||
.icon-failures-true:before {
|
||||
content: "\f06a";
|
||||
}
|
||||
|
||||
.icon-failures-none:before,
|
||||
.icon-failures-false:before {
|
||||
content: "\f111";
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 3px 4px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Inventory job status badge */
|
||||
.failures-true {
|
||||
background-color: @red;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.failures-false {
|
||||
background-color: @green;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Cloud inventory status. i.e. inventory_source.status values */
|
||||
|
||||
@ -671,7 +721,7 @@ select.field-mini-height {
|
||||
content: "\f0c2";
|
||||
}
|
||||
.icon-cloud-na {
|
||||
color: #e3e3e3;
|
||||
color: #888;
|
||||
}
|
||||
.icon-cloud-never {
|
||||
color: #888;
|
||||
@ -684,7 +734,6 @@ select.field-mini-height {
|
||||
color: @red;
|
||||
}
|
||||
|
||||
|
||||
.field-success {
|
||||
color: #5bb75b;
|
||||
}
|
||||
@ -702,11 +751,10 @@ select.field-mini-height {
|
||||
}
|
||||
|
||||
.field-badge {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.job-changed,
|
||||
.license-warning,
|
||||
.license-warning
|
||||
.license-demo {
|
||||
color: @warning;
|
||||
}
|
||||
|
||||
@ -1272,7 +1272,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
|
||||
|
||||
html += "<div class=\"hosts-well well\">\n";
|
||||
|
||||
html += SearchWidget({ iterator: form.iterator, template: form, mini: true, size: 'col-md-6 col-lg-6'});
|
||||
html += SearchWidget({ iterator: form.iterator, template: form, mini: true, size: 'col-md-5 col-lg-5'});
|
||||
|
||||
html += "<div class=\"col-md-5 col-lg-5\">\n"
|
||||
html += "<div class=\"pull-right\">\n";
|
||||
@ -1309,26 +1309,28 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
|
||||
html += "<tr>\n";
|
||||
|
||||
for (var fld in form.fields) {
|
||||
html += "<th class=\"list-header\" id=\"" + fld + "-header\" ";
|
||||
html += (!form.fields[fld].nosort) ? "ng-click=\"sort('"+ fld + "')\"" : "";
|
||||
html += ">";
|
||||
html += (form['fields'][fld].label && form['fields'][fld].type !== 'DropDown') ? form['fields'][fld].label : '';
|
||||
if (form.fields[fld].nosort == undefined || form.fields[fld].nosort == false) {
|
||||
html += " <i class=\"";
|
||||
if (form.fields[fld].key) {
|
||||
if (form.fields[fld].desc) {
|
||||
html += "icon-sort-down";
|
||||
}
|
||||
else {
|
||||
html += "icon-sort-up";
|
||||
}
|
||||
}
|
||||
else {
|
||||
html += "icon-sort";
|
||||
}
|
||||
html += "\"></i>";
|
||||
if (form.fields[fld].searchOnly == undefined || form.fields[fld].searchOnly == false) {
|
||||
html += "<th class=\"list-header\" id=\"" + fld + "-header\" ";
|
||||
html += (!form.fields[fld].nosort) ? "ng-click=\"sort('"+ fld + "')\"" : "";
|
||||
html += ">";
|
||||
html += (form['fields'][fld].label && form['fields'][fld].type !== 'DropDown') ? form['fields'][fld].label : '';
|
||||
if (form.fields[fld].nosort == undefined || form.fields[fld].nosort == false) {
|
||||
html += " <i class=\"";
|
||||
if (form.fields[fld].key) {
|
||||
if (form.fields[fld].desc) {
|
||||
html += "icon-sort-down";
|
||||
}
|
||||
else {
|
||||
html += "icon-sort-up";
|
||||
}
|
||||
}
|
||||
else {
|
||||
html += "icon-sort";
|
||||
}
|
||||
html += "\"></i>";
|
||||
}
|
||||
html += "</th>\n";
|
||||
}
|
||||
html += "</th>\n";
|
||||
}
|
||||
|
||||
html += "<th></th>\n";
|
||||
@ -1349,11 +1351,11 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
|
||||
rfield = form.fields[fld];
|
||||
if (fld == 'groups' ) {
|
||||
// generate group form control/button widget
|
||||
html += "<td>";
|
||||
html += "<td class=\"col-lg-5 col-md-4 col-sm-3\">";
|
||||
html += "<div class=\"input-group input-group-sm\">\n";
|
||||
html += "<span class=\"input-group-btn\">\n";
|
||||
html += "<button class=\"btn btn-default\" type=\"button\" id=\"edit_groups_btn\" ng-click=\"editHostGroups({{ host.id }})\" " +
|
||||
"aw-tool-tip=\"Change group associations for this host\" data-placement=\"top\" >" +
|
||||
"aw-tool-tip=\"Edit group associations\" data-placement=\"top\" >" +
|
||||
"<i class=\"icon-sitemap\"></i></button>\n";
|
||||
html += "</span>\n";
|
||||
html += "<input type=\"text\" id=\"host_groups\" ng-model=\"host.groups\" class=\"form-control\" disabled=\"disabled\" >\n";
|
||||
@ -1361,7 +1363,9 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
|
||||
html += "</td>\n";
|
||||
}
|
||||
else {
|
||||
html += Column({ list: form, fld: fld, options: options, base: null });
|
||||
if (form.fields[fld].searchOnly == undefined || form.fields[fld].searchOnly == false) {
|
||||
html += Column({ list: form, fld: fld, options: options, base: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1404,12 +1408,14 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
|
||||
html += "</table>\n";
|
||||
html += "</div>\n"; // close list
|
||||
|
||||
/*
|
||||
html += "<div class=\"row host-failure-filter\">\n";
|
||||
html += "<div class=\"col-lg-12\">\n";
|
||||
html += "<label class=\"checkbox-inline pull-right\"><input type=\"checkbox\" ng-model=\"hostFailureFilter\" ng-change=\"filterHosts()\" > Only show hosts with failed jobs" +
|
||||
"</label>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
*/
|
||||
|
||||
html += "</div>\n"; // close well
|
||||
|
||||
|
||||
@ -186,7 +186,13 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
|
||||
var name = field['label'].replace(/ /g,'_');
|
||||
|
||||
html = (params.td == undefined || params.td !== false) ? "<td>\n" : "";
|
||||
if (params.td == undefined || params.td !== false) {
|
||||
html = "<td class=\"" + fld + "-column\">\n";
|
||||
}
|
||||
else {
|
||||
html = '';
|
||||
}
|
||||
|
||||
/*
|
||||
html += "<div class=\"btn-group\">\n";
|
||||
html += "<button type=\"button\" ";
|
||||
@ -218,19 +224,49 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
html += (field.options[i].ngHide) ? "ng-hide=\"" + field.options[i].ngHide + "\" " : "";
|
||||
html += "href=\"\">" + field.options[i].label + "</a></li>\n";
|
||||
}
|
||||
|
||||
html += "</ul>\n";
|
||||
html += "</div>\n";
|
||||
|
||||
html += (params.td == undefined || params.td !== false) ? "</td>\n" : "";
|
||||
|
||||
|
||||
return html;
|
||||
|
||||
}
|
||||
}])
|
||||
|
||||
.factory('BadgeCount', [ function() {
|
||||
return function(params) {
|
||||
|
||||
// Adds a badge count with optional tooltip
|
||||
|
||||
var list = params['list'];
|
||||
var fld = params['fld'];
|
||||
var field = list.fields[fld];
|
||||
var options = params['options'];
|
||||
var base = params['base'];
|
||||
var html = "<td class=\"" + fld + "-column"
|
||||
html += (field.columnClass) ? " " + field.columnClass : "";
|
||||
html += "\">\n";
|
||||
html += "<a ng-href=\"" + field.ngHref + "\" aw-tool-tip=\"" + field.awToolTip + "\"";
|
||||
html += (field.dataPlacement) ? " data-placement=\"" + field.dataPlacement + "\"" : "";
|
||||
html += ">";
|
||||
html += "<span class=\"badge";
|
||||
html += (field['class']) ? " " + field['class'] : "";
|
||||
html += "\">";
|
||||
html += "\{\{ " + list.iterator + '.' + fld + " \}\}";
|
||||
html += "</span>";
|
||||
html += (field.badgeLabel) ? " " + field.badgeLabel : "";
|
||||
html += "</a>\n";
|
||||
html += "</td>\n";
|
||||
return html;
|
||||
}
|
||||
}])
|
||||
|
||||
.factory('Badge', [ function() {
|
||||
return function(field) {
|
||||
|
||||
// Adds an icon(s) with optional tooltip
|
||||
|
||||
var html = '';
|
||||
|
||||
if (field.badges) {
|
||||
@ -254,7 +290,10 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
}
|
||||
else {
|
||||
if (field.badgeToolTip) {
|
||||
html += "<a href=\"\" aw-tool-tip=\"" + field.badgeToolTip + "\"";
|
||||
html += "<a ";
|
||||
html += (field.badgeNgHref) ? "ng-href=\"" + field.badgeNgHref + "\" " : "href=\"\"";
|
||||
html += (field.ngClick) ? "ng-click=\"" + field.ngClick + "\" " : "";
|
||||
html += " aw-tool-tip=\"" + field.badgeToolTip + "\"";
|
||||
html += (field.badgeTipPlacement) ? " data-placement=\"" + field.badgeTipPlacement + "\"" : "";
|
||||
html += (field.badgeShow) ? " ng-show=\"" + field.badgeShow + "\"" : "";
|
||||
html += ">";
|
||||
@ -275,7 +314,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
}
|
||||
}])
|
||||
|
||||
.factory('Column', ['Attr', 'Icon', 'DropDown', 'Badge', function(Attr, Icon, DropDown, Badge) {
|
||||
.factory('Column', ['Attr', 'Icon', 'DropDown', 'Badge', 'BadgeCount', function(Attr, Icon, DropDown, Badge, BadgeCount) {
|
||||
return function(params) {
|
||||
var list = params['list'];
|
||||
var fld = params['fld'];
|
||||
@ -288,6 +327,9 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
if (field.type !== undefined && field.type == 'DropDown') {
|
||||
html = DropDown(params);
|
||||
}
|
||||
else if (field.type == 'badgeCount') {
|
||||
html = BadgeCount(params);
|
||||
}
|
||||
else {
|
||||
html += "<td class=\"" + fld + "-column";
|
||||
html += (field['class']) ? " " + field['class'] : "";
|
||||
@ -314,17 +356,21 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
}
|
||||
|
||||
// Start the Link
|
||||
if ( (field.key || field.link || field.linkTo || field.ngClick ) &&
|
||||
if ( (field.key || field.link || field.linkTo || field.ngClick || field.ngHref) &&
|
||||
options['mode'] != 'lookup' && options['mode'] != 'select' && !field.noLink ) {
|
||||
var cap=false;
|
||||
if (field.linkTo) {
|
||||
html += "<a href=\"#" + field.linkTo + "\" ";
|
||||
html += "<a href=\"" + field.linkTo + "\" ";
|
||||
cap = true;
|
||||
}
|
||||
else if (field.ngClick) {
|
||||
html += "<a href=\"\"" + Attr(field, 'ngClick') + " ";
|
||||
cap = true;
|
||||
}
|
||||
else if (field.ngHref) {
|
||||
html += "<a ng-href=\"" + field.ngHref + "\" ";
|
||||
cap = true;
|
||||
}
|
||||
else if (field.link || (field.key && (field.link === undefined || field.link))) {
|
||||
html += "<a href=\"#/" + base + "/{{" + list.iterator + ".id }}\" ";
|
||||
cap = true;
|
||||
@ -368,7 +414,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
}
|
||||
|
||||
// close the link
|
||||
if ( (field.key || field.link || field.linkTo || field.ngClick )
|
||||
if ( (field.key || field.link || field.linkTo || field.ngClick || field.ngHref )
|
||||
&& options.mode != 'lookup' && options.mode != 'select' && !field.noLink ) {
|
||||
html += "</a>";
|
||||
}
|
||||
@ -458,7 +504,6 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
//html += "</a>\n";
|
||||
|
||||
html += "<ul class=\"dropdown-menu\" id=\"" + iterator + "SearchDropdown\">\n";
|
||||
|
||||
for ( var fld in form.fields) {
|
||||
if (form.fields[fld].searchable == undefined || form.fields[fld].searchable == true) {
|
||||
html += "<li><a href=\"\" ng-click=\"setSearchField('" + iterator + "','";
|
||||
@ -475,8 +520,9 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
|
||||
html += "<input id=\"search_value_input\" type=\"text\" ng-hide=\"" + iterator + "SelectShow || " + iterator + "InputHide\" class=\"form-control ";
|
||||
html += "\" ng-model=\"" + iterator + "SearchValue\" ng-change=\"search('" + iterator +
|
||||
"')\" placeholder=\"Search\" type=\"text\" >\n";
|
||||
"')\" placeholder=\"Search\" type=\"text\" ng-disabled=\"" + iterator + "InputDisable\">\n";
|
||||
|
||||
/*
|
||||
html += "<div class=\"input-group-btn dropdown\">\n";
|
||||
html += "<button type=\"button\" ";
|
||||
html += "id=\"search_option_ddown\" ";
|
||||
@ -490,7 +536,17 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers'])
|
||||
html += "<li><a href=\"\" ng-click=\"setSearchType('" + iterator + "','icontains','Contains')\">Contains</a></li>\n";
|
||||
html += "</ul>\n";
|
||||
html += "</div><!-- input-group-btn -->\n";
|
||||
*/
|
||||
|
||||
|
||||
// Reset button
|
||||
html += "<div class=\"input-group-btn\">\n";
|
||||
html += "<button type=\"button\" class=\"btn btn-default btn-small\" ng-click=\"resetSearch('" + iterator + "')\" " +
|
||||
"aw-tool-tip=\"Reset filter\" data-placement=\"top\" " +
|
||||
"><i class=\"icon-undo\"></i></button>\n";
|
||||
html += "</div><!-- input-group-btn -->\n";
|
||||
html += "</div><!-- input-group -->\n";
|
||||
|
||||
html += "</div><!-- col-lg-x -->\n";
|
||||
html += "<div class=\"col-lg-1 col-md-1 col-sm-1 col-xs-1\"><i class=\"icon-spinner icon-spin icon-large\" ng-show=\"" + iterator +
|
||||
"SearchSpin == true\"></i></div>\n";
|
||||
|
||||
@ -224,10 +224,10 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
|
||||
}
|
||||
}
|
||||
|
||||
if (list.name == 'inventories' && options.mode !== 'select') {
|
||||
/*if (list.name == 'inventories' && options.mode !== 'select') {
|
||||
html += "<label class=\"checkbox-inline pull-right\"><input type=\"checkbox\" ng-model=\"inventoryFailureFilter\" " +
|
||||
"ng-change=\"search('inventory')\" id=\"failed_jobs_chbox\"> Show only inventories with failed jobs</label>\n";
|
||||
}
|
||||
}*/
|
||||
|
||||
//select instructions
|
||||
if (options.mode == 'select' && list.selectInstructions) {
|
||||
|
||||
@ -106,6 +106,7 @@
|
||||
<script src="{{ STATIC_URL }}js/helpers/Selection.js"></script>
|
||||
<script src="{{ STATIC_URL }}js/helpers/Projects.js"></script>
|
||||
<script src="{{ STATIC_URL }}js/helpers/Users.js"></script>
|
||||
<script src="{{ STATIC_URL }}js/helpers/Jobs.js"></script>
|
||||
<script src="{{ STATIC_URL }}js/widgets/ObjectCount.js"></script>
|
||||
|
||||
<script src="{{ STATIC_URL }}lib/less/less-1.4.1.min.js"></script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user