Merge pull request #2110 from mabashian/1928-organizations-action-buttons-v2

Organization lists bug fixes
This commit is contained in:
Michael Abashian 2016-06-02 09:34:58 -04:00
commit 854b613bb7
14 changed files with 1135 additions and 129 deletions

View File

@ -976,6 +976,7 @@ input[type="checkbox"].checkbox-no-label {
/* Display list actions next to search widget */
.list-actions {
text-align: right;
margin-bottom: 20px;
.fa-lg {
vertical-align: -8%;

View File

@ -112,7 +112,9 @@ export default
// use $state.go with reload: true option to re-instantiate sockets in
$state.go('jobDetail', {id: job}, {reload: true});
}
scope.clearDialog();
if(scope.clearDialog) {
scope.clearDialog();
}
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',

View File

@ -19,6 +19,6 @@ export default {
},
ncyBreadcrumb: {
parent: "organizations",
label: "{{name}}"
label: "{{organization_name}}"
}
};

View File

@ -5,7 +5,7 @@
<div class="AddUsers-header">
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">{{ $parent.org_name }}<div class="List-titleLockup"></div>Add {{ addType }}
<div class="List-titleText ng-binding">{{ $parent.organization_name }}<div class="List-titleLockup"></div>Add {{ addType }}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -0,0 +1,101 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$stateParams', '$scope', 'UserList', 'Rest', '$state', 'generateList', '$compile',
'SearchInit', 'PaginateInit', 'Wait', 'Prompt', 'ProcessErrors', 'GetBasePath',
function($stateParams, $scope, UserList, Rest, $state, GenerateList, $compile,
SearchInit, PaginateInit, Wait, Prompt, ProcessErrors, GetBasePath) {
var list,
url,
generator = GenerateList,
orgBase = GetBasePath('organizations');
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>ADMINS";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'admins';
$scope.organization_name = data.name;
$scope.org_id = data.id;
var listMode = 'users';
list = _.cloneDeep(UserList);
list.emptyListText = "Please add items to this list";
delete list.actions.add;
list.searchRowActions = {
add: {
buttonContent: '&#43; ADD administrator',
awToolTip: 'Add existing user to organization as administrator',
actionClass: 'btn List-buttonSubmit',
ngClick: 'addUsers()'
}
};
url = data.related.admins;
list.listTitle = listTitle;
list.basePath = url;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
SearchInit({
scope: $scope,
set: listMode,
list: list,
url: url
});
PaginateInit({
scope: $scope,
list: list,
url: url
});
$scope.search(list.iterator);
});
$scope.addUsers = function () {
$compile("<add-users class='AddUsers'></add-users>")($scope);
};
$scope.editUser = function (id) {
$state.transitionTo('users.edit', {user_id: id});
};
$scope.deleteUser = function (id, name) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
var url = orgBase + $stateParams.organization_id + '/admins/';
Rest.setUrl(url);
Rest.post({
id: id,
disassociate: true
}).success(function () {
$scope.search(list.iterator);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to remove the following administrator from this organization?</div><div class="Prompt-bodyTarget">' + name + '</div>',
action: action,
actionText: 'DELETE'
});
};
$scope.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -0,0 +1,343 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList',
'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller',
'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state',
function($scope, $rootScope, $location, $log,
$stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList,
generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, Wait,
Find, Empty, $state) {
var list,
invUrl,
orgBase = GetBasePath('organizations'),
generator = generateList;
// Go out and get the organization
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>INVENTORIES";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'inventories';
$scope.organization_name = data.name;
$scope.org_id = data.id;
list = _.cloneDeep(InventoryList);
list.emptyListText = "This list is populated by inventories added from the&nbsp;<a ui-sref='inventories.add'>Inventories</a>&nbsp;section";
delete list.actions.add;
delete list.fieldActions.delete;
invUrl = data.related.inventories;
list.listTitle = listTitle;
list.basePath = invUrl;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
$rootScope.flashMessage = null;
SearchInit({
scope: $scope,
set: 'inventories',
list: list,
url: invUrl
});
PaginateInit({
scope: $scope,
list: list,
url: invUrl
});
if ($stateParams.name) {
$scope[InventoryList.iterator + 'InputDisable'] = false;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.name;
$scope[InventoryList.iterator + 'SearchField'] = 'name';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.name.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = null;
}
if ($stateParams.has_active_failures) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_active_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'has_active_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_active_failures.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_active_failures === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.has_inventory_sources) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_inventory_sources;
$scope[InventoryList.iterator + 'SearchField'] = 'has_inventory_sources';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_inventory_sources.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_inventory_sources === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.inventory_sources_with_failures) {
// pass a value of true, however this field actually contains an integer value
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.inventory_sources_with_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'inventory_sources_with_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.inventory_sources_with_failures.label;
$scope[InventoryList.iterator + 'SearchType'] = 'gtzero';
}
$scope.search(list.iterator);
});
function ellipsis(a) {
if (a.length > 20) {
return a.substr(0,20) + '...';
}
return a;
}
function attachElem(event, html, title) {
var elem = $(event.target).parent();
try {
elem.tooltip('hide');
elem.popover('destroy');
}
catch(err) {
//ignore
}
$('.popover').each(function() {
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
$(this).remove();
});
$('.tooltip').each( function() {
// close any lingering tool tipss
$(this).hide();
});
elem.attr({
"aw-pop-over": html,
"data-popover-title": title,
"data-placement": "right" });
$compile(elem)($scope);
elem.on('shown.bs.popover', function() {
$('.popover').each(function() {
$compile($(this))($scope); //make nested directives work!
});
$('.popover-content, .popover-title').click(function() {
elem.popover('hide');
});
});
elem.popover('show');
}
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
//If we got here by deleting an inventory, stop the spinner and cleanup events
Wait('stop');
try {
$('#prompt-modal').modal('hide');
}
catch(e) {
// ignore
}
$scope.inventories.forEach(function(inventory, idx) {
$scope.inventories[idx].launch_class = "";
if (inventory.has_inventory_sources) {
if (inventory.inventory_sources_with_failures > 0) {
$scope.inventories[idx].syncStatus = 'error';
$scope.inventories[idx].syncTip = inventory.inventory_sources_with_failures + ' groups with sync failures. Click for details';
}
else {
$scope.inventories[idx].syncStatus = 'successful';
$scope.inventories[idx].syncTip = 'No inventory sync failures. Click for details.';
}
}
else {
$scope.inventories[idx].syncStatus = 'na';
$scope.inventories[idx].syncTip = 'Not configured for inventory sync.';
$scope.inventories[idx].launch_class = "btn-disabled";
}
if (inventory.has_active_failures) {
$scope.inventories[idx].hostsStatus = 'error';
$scope.inventories[idx].hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.';
}
else if (inventory.total_hosts) {
$scope.inventories[idx].hostsStatus = 'successful';
$scope.inventories[idx].hostsTip = 'No hosts with failures. Click for details.';
}
else {
$scope.inventories[idx].hostsStatus = 'none';
$scope.inventories[idx].hostsTip = 'Inventory contains 0 hosts.';
}
});
});
if ($scope.removeRefreshInventories) {
$scope.removeRefreshInventories();
}
$scope.removeRefreshInventories = $scope.$on('RefreshInventories', function () {
// Reflect changes after inventory properties edit completes
$scope.search(list.iterator);
});
if ($scope.removeHostSummaryReady) {
$scope.removeHostSummaryReady();
}
$scope.removeHostSummaryReady = $scope.$on('HostSummaryReady', function(e, event, data) {
var html, title = "Recent Jobs";
Wait('stop');
if (data.count > 0) {
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Finished</th>";
html += "<th>Name</th>";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
data.results.forEach(function(row) {
html += "<tr>\n";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>\n";
html += "<td>" + ($filter('longDate')(row.finished)).replace(/ /,'<br />') + "</td>";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
". Click for details\" aw-tip-placement=\"top\">" + ellipsis(row.name) + "</a></td>";
html += "</tr>\n";
});
html += "</tbody>\n";
html += "</table>\n";
}
else {
html = "<p>No recent job data available for this inventory.</p>\n";
}
attachElem(event, html, title);
});
if ($scope.removeGroupSummaryReady) {
$scope.removeGroupSummaryReady();
}
$scope.removeGroupSummaryReady = $scope.$on('GroupSummaryReady', function(e, event, inventory, data) {
var html, title;
Wait('stop');
// Build the html for our popover
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Last Sync</th>";
html += "<th>Group</th>";
html += "</tr>";
html += "</thead>\n";
html += "<tbody>\n";
data.results.forEach( function(row) {
if (row.related.last_update) {
html += "<tr>";
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\" aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) + ". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>";
html += "<td>" + ($filter('longDate')(row.last_updated)).replace(/ /,'<br />') + "</td>";
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
html += "</tr>\n";
}
else {
html += "<tr>";
html += "<td><a href=\"\" aw-tool-tip=\"No sync data\" aw-tip-placement=\"top\"><i class=\"fa icon-job-none\"></i></a></td>";
html += "<td>NA</td>";
html += "<td><a href=\"\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
html += "</tr>\n";
}
});
html += "</tbody>\n";
html += "</table>\n";
title = "Sync Status";
attachElem(event, html, title);
});
$scope.showGroupSummary = function(event, id) {
var inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.syncStatus !== 'na') {
Wait('start');
Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5');
Rest.get()
.success(function(data) {
$scope.$emit('GroupSummaryReady', event, inventory, data);
})
.error(function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status
});
});
}
}
};
$scope.showHostSummary = function(event, id) {
var url, inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.total_hosts > 0) {
Wait('start');
url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed=";
url += (inventory.has_active_failures) ? 'true' : "false";
url += "&order_by=-finished&page_size=5";
Rest.setUrl(url);
Rest.get()
.success( function(data) {
$scope.$emit('HostSummaryReady', event, data);
})
.error( function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. GET returned: ' + status
});
});
}
}
};
$scope.viewJob = function(url) {
// Pull the id out of the URL
var id = url.replace(/^\//, '').split('/')[3];
$state.go('inventorySyncStdout', {id: id});
};
$scope.editInventory = function (id) {
$state.go('inventories.edit', {inventory_id: id});
};
$scope.manageInventory = function(id){
$location.path($location.path() + '/' + id + '/manage');
};
// 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.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -0,0 +1,91 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', 'Rest', 'Alert', 'JobTemplateList', 'generateList',
'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList',
'LookUpInit', 'InitiatePlaybookRun', 'Wait', '$compile',
'$state',
function($scope, $rootScope, $location, $log,
$stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt,
SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors,
GetBasePath, JobTemplateForm, CredentialList, LookUpInit, InitiatePlaybookRun,
Wait, $compile, $state) {
var list,
jobTemplateUrl,
generator = GenerateList,
orgBase = GetBasePath('organizations');
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>JOB TEMPLATES";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'job_templates';
$scope.organization_name = data.name;
$scope.org_id = data.id;
list = _.cloneDeep(JobTemplateList);
list.emptyListText = "This list is populated by job templates added from the&nbsp;<a ui-sref='jobTemplates.add'>Job Templates</a>&nbsp;section";
delete list.actions.add;
delete list.fieldActions.delete;
jobTemplateUrl = "/api/v1/job_templates/?project__organization=" + data.id;
list.listTitle = listTitle;
list.basePath = jobTemplateUrl;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
// Cleanup after a delete
Wait('stop');
$('#prompt-modal').modal('hide');
});
SearchInit({
scope: $scope,
set: 'job_templates',
list: list,
url: jobTemplateUrl
});
PaginateInit({
scope: $scope,
list: list,
url: jobTemplateUrl
});
$scope.search(list.iterator);
});
$scope.addJobTemplate = function () {
$state.transitionTo('jobTemplates.add');
};
$scope.editJobTemplate = function (id) {
$state.transitionTo('jobTemplates.edit', {template_id: id});
};
$scope.submitJob = function (id) {
InitiatePlaybookRun({ scope: $scope, id: id });
};
$scope.scheduleJob = function (id) {
$state.go('jobTemplateSchedules', {id: id});
};
$scope.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -0,0 +1,350 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', 'Rest', 'Alert', 'ProjectList', 'generateList', 'Prompt',
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate',
'Refresh', 'Wait', 'GetChoices', 'Empty', 'Find',
'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state',
function($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, ProjectList, GenerateList, Prompt, SearchInit,
PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath,
SelectionInit, ProjectUpdate, Refresh, Wait, GetChoices, Empty,
Find, GetProjectIcon, GetProjectToolTip, $filter, $state) {
var list,
projUrl,
choiceCount = 0,
orgBase = GetBasePath('organizations'),
projBase = GetBasePath('projects'),
generator = GenerateList;
// Go out and get the organization
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>PROJECTS";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'projects';
$scope.organization_name = data.name;
$scope.org_id = data.id;
list = _.cloneDeep(ProjectList);
list.emptyListText = "This list is populated by projects added from the&nbsp;<a ui-sref='projects.add'>Projects</a>&nbsp;section";
delete list.actions.add;
delete list.fieldActions.delete;
projUrl = data.related.projects;
list.listTitle = listTitle;
list.basePath = projUrl;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
$rootScope.flashMessage = null;
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
Wait('stop');
if ($scope.projects) {
$scope.projects.forEach(function(project, i) {
$scope.projects[i].statusIcon = GetProjectIcon(project.status);
$scope.projects[i].statusTip = GetProjectToolTip(project.status);
$scope.projects[i].scm_update_tooltip = "Start an SCM update";
$scope.projects[i].scm_schedule_tooltip = "Schedule future SCM updates";
$scope.projects[i].scm_type_class = "";
if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') {
$scope.projects[i].statusTip = 'Canceled. Click for details';
}
if (project.status === 'running' || project.status === 'updating') {
$scope.projects[i].scm_update_tooltip = "SCM update currently running";
$scope.projects[i].scm_type_class = "btn-disabled";
}
$scope.project_scm_type_options.forEach(function(type) {
if (type.value === project.scm_type) {
$scope.projects[i].scm_type = type.label;
if (type.label === 'Manual') {
$scope.projects[i].scm_update_tooltip = 'Manual projects do not require an SCM update';
$scope.projects[i].scm_schedule_tooltip = 'Manual projects do not require a schedule';
$scope.projects[i].scm_type_class = 'btn-disabled';
$scope.projects[i].statusTip = 'Not configured for SCM';
$scope.projects[i].statusIcon = 'none';
}
}
});
});
}
});
// Handle project update status changes
if ($rootScope.removeJobStatusChange) {
$rootScope.removeJobStatusChange();
}
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-projects', function(e, data) {
var project;
$log.debug(data);
if ($scope.projects) {
// Assuming we have a list of projects available
project = Find({ list: $scope.projects, key: 'id', val: data.project_id });
if (project) {
// And we found the affected project
$log.debug('Received event for project: ' + project.name);
$log.debug('Status changed to: ' + data.status);
if (data.status === 'successful' || data.status === 'failed') {
$scope.search(list.iterator, null, null, null, null, false);
}
else {
project.scm_update_tooltip = "SCM update currently running";
project.scm_type_class = "btn-disabled";
}
project.status = data.status;
project.statusIcon = GetProjectIcon(data.status);
project.statusTip = GetProjectToolTip(data.status);
}
}
});
if ($scope.removeChoicesHere) {
$scope.removeChoicesHere();
}
$scope.removeChoicesHere = $scope.$on('choicesCompleteProjectList', function () {
var opt;
list.fields.scm_type.searchOptions = $scope.project_scm_type_options;
list.fields.status.searchOptions = $scope.project_status_options;
if ($stateParams.scm_type && $stateParams.status) {
// Request coming from home page. User wants all errors for an scm_type
projUrl += '?status=' + $stateParams.status;
}
SearchInit({
scope: $scope,
set: 'projects',
list: list,
url: projUrl
});
PaginateInit({
scope: $scope,
list: list,
url: projUrl
});
if ($stateParams.scm_type) {
$scope[list.iterator + 'SearchType'] = '';
$scope[list.iterator + 'SearchField'] = 'scm_type';
$scope[list.iterator + 'SelectShow'] = true;
$scope[list.iterator + 'SearchSelectOpts'] = list.fields.scm_type.searchOptions;
$scope[list.iterator + 'SearchFieldLabel'] = list.fields.scm_type.label.replace(/<br\>/g, ' ');
for (opt in list.fields.scm_type.searchOptions) {
if (list.fields.scm_type.searchOptions[opt].value === $stateParams.scm_type) {
$scope[list.iterator + 'SearchSelectValue'] = list.fields.scm_type.searchOptions[opt];
break;
}
}
} else if ($stateParams.status) {
$scope[list.iterator + 'SearchType'] = '';
$scope[list.iterator + 'SearchValue'] = $stateParams.status;
$scope[list.iterator + 'SearchField'] = 'status';
$scope[list.iterator + 'SelectShow'] = true;
$scope[list.iterator + 'SearchFieldLabel'] = list.fields.status.label;
$scope[list.iterator + 'SearchSelectOpts'] = list.fields.status.searchOptions;
for (opt in list.fields.status.searchOptions) {
if (list.fields.status.searchOptions[opt].value === $stateParams.status) {
$scope[list.iterator + 'SearchSelectValue'] = list.fields.status.searchOptions[opt];
break;
}
}
}
$scope.search(list.iterator);
});
if ($scope.removeChoicesReadyList) {
$scope.removeChoicesReadyList();
}
$scope.removeChoicesReadyList = $scope.$on('choicesReadyProjectList', function () {
choiceCount++;
if (choiceCount === 2) {
$scope.$emit('choicesCompleteProjectList');
}
});
// Load options for status --used in search
GetChoices({
scope: $scope,
url: projBase,
field: 'status',
variable: 'project_status_options',
callback: 'choicesReadyProjectList'
});
// Load the list of options for Kind
GetChoices({
scope: $scope,
url: projBase,
field: 'scm_type',
variable: 'project_scm_type_options',
callback: 'choicesReadyProjectList'
});
});
$scope.editProject = function (id) {
$state.transitionTo('projects.edit', {id: id});
};
if ($scope.removeGoToJobDetails) {
$scope.removeGoToJobDetails();
}
$scope.removeGoToJobDetails = $scope.$on('GoToJobDetails', function(e, data) {
if (data.summary_fields.current_update || data.summary_fields.last_update) {
Wait('start');
// Grab the id from summary_fields
var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id;
$state.go('scmUpdateStdout', {id: id});
} else {
Alert('No Updates Available', 'There is no SCM update information available for this project. An update has not yet been ' +
' completed. If you have not already done so, start an update for this project.', 'alert-info');
}
});
$scope.showSCMStatus = function (id) {
// Refresh the project list
var project = Find({ list: $scope.projects, key: 'id', val: id });
if (Empty(project.scm_type) || project.scm_type === 'Manual') {
Alert('No SCM Configuration', 'The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, ' +
'and then run an update.', 'alert-info');
} else {
// Refresh what we have in memory to insure we're accessing the most recent status record
Rest.setUrl(project.url);
Rest.get()
.success(function(data) {
$scope.$emit('GoToJobDetails', data);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Project lookup failed. GET returned: ' + status });
});
}
};
if ($scope.removeCancelUpdate) {
$scope.removeCancelUpdate();
}
$scope.removeCancelUpdate = $scope.$on('Cancel_Update', function (e, url) {
// Cancel the project update process
Rest.setUrl(url);
Rest.post()
.success(function () {
Alert('SCM Update Cancel', 'Your request to cancel the update was submitted to the task manager.', 'alert-info');
$scope.refresh();
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST status: ' + status });
});
});
if ($scope.removeCheckCancel) {
$scope.removeCheckCancel();
}
$scope.removeCheckCancel = $scope.$on('Check_Cancel', function (e, data) {
// Check that we 'can' cancel the update
var url = data.related.cancel;
Rest.setUrl(url);
Rest.get()
.success(function (data) {
if (data.can_cancel) {
$scope.$emit('Cancel_Update', url);
} else {
Alert('Cancel Not Allowed', 'Either you do not have access or the SCM update process completed. ' +
'Click the <em>Refresh</em> button to view the latest status.', 'alert-info', null, null, null, null, true);
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. GET status: ' + status });
});
});
$scope.cancelUpdate = function (id, name) {
Rest.setUrl(GetBasePath("projects") + id);
Rest.get()
.success(function (data) {
if (data.related.current_update) {
Rest.setUrl(data.related.current_update);
Rest.get()
.success(function (data) {
$scope.$emit('Check_Cancel', data);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + data.related.current_update + ' failed. GET status: ' + status });
});
} else {
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + $filter('sanitize')(name) + '. Click the <em>Refresh</em> ' +
'button to view the latest status.', 'alert-info',undefined,undefined,undefined,undefined,true);
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to get project failed. GET status: ' + status });
});
};
$scope.refresh = function () {
$scope.search(list.iterator);
};
$scope.SCMUpdate = function (project_id, event) {
try {
$(event.target).tooltip('hide');
}
catch(e) {
// ignore
}
$scope.projects.every(function(project) {
if (project.id === project_id) {
if (project.scm_type === "Manual" || Empty(project.scm_type)) {
// Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event
// to work. So user can click, but we just won't do anything.
//Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info');
} else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') {
// Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info');
} else {
ProjectUpdate({ scope: $scope, project_id: project.id });
}
return false;
}
return true;
});
};
$scope.editSchedules = function(id) {
var project = Find({ list: $scope.projects, key: 'id', val: id });
if (project.scm_type === "Manual" || Empty(project.scm_type)) {
// Nothing to do
}
else {
$location.path('/projects/' + id + '/schedules');
}
};
$scope.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -0,0 +1,93 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope', '$location', '$log', '$stateParams',
'Rest', 'Alert', 'TeamList', 'generateList', 'Prompt', 'SearchInit', 'PaginateInit',
'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'SetTeamListeners', 'GetBasePath',
'SelectionInit', 'Wait', '$state', 'Refresh',
function($scope, $rootScope, $location, $log, $stateParams,
Rest, Alert, TeamList, GenerateList, Prompt, SearchInit, PaginateInit,
ReturnToCaller, ClearScope, ProcessErrors, SetTeamListeners, GetBasePath,
SelectionInit, Wait, $state, Refresh) {
var list,
teamUrl,
orgBase = GetBasePath('organizations'),
generator = GenerateList;
// Go out and get the organization
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>TEAMS";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'teams';
$scope.organization_name = data.name;
$scope.org_id = data.id;
list = _.cloneDeep(TeamList);
list.emptyListText = "This list is populated by teams added from the&nbsp;<a ui-sref='teams.add'>Teams</a>&nbsp;section";
delete list.actions.add;
delete list.fieldActions.delete;
teamUrl = data.related.teams;
list.listTitle = listTitle;
list.basePath = teamUrl;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
$rootScope.flashMessage = null;
$scope.$on("RefreshTeamsList", function() {
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
Refresh({
scope: $scope,
set: 'teams',
iterator: 'team',
url: GetBasePath('teams') + "?order_by=name&page_size=" + $scope.team_page_size
});
});
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
// After a refresh, populate the organization name on each row
var i;
if ($scope.teams) {
for (i = 0; i < $scope.teams.length; i++) {
if ($scope.teams[i].summary_fields.organization) {
$scope.teams[i].organization_name = $scope.teams[i].summary_fields.organization.name;
}
}
}
});
SearchInit({
scope: $scope,
set: 'teams',
list: list,
url: teamUrl
});
PaginateInit({
scope: $scope,
list: list,
url: teamUrl
});
$scope.search(list.iterator);
});
$scope.editTeam = function (id) {
$state.transitionTo('teams.edit', {team_id: id});
};
$scope.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -0,0 +1,100 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$stateParams', '$scope', 'UserList', 'Rest', '$state', 'generateList', '$compile',
'SearchInit', 'PaginateInit', 'Wait', 'Prompt', 'ProcessErrors', 'GetBasePath',
function($stateParams, $scope, UserList, Rest, $state, GenerateList, $compile,
SearchInit, PaginateInit, Wait, Prompt, ProcessErrors, GetBasePath) {
var list,
url,
generator = GenerateList,
orgBase = GetBasePath('organizations');
Rest.setUrl(orgBase + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var listTitle = data.name + "<div class='List-titleLockup'></div>USERS";
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = 'users';
$scope.organization_name = data.name;
$scope.org_id = data.id;
list = _.cloneDeep(UserList);
list.emptyListText = "Please add items to this list";
delete list.actions.add;
list.searchRowActions = {
add: {
buttonContent: '&#43; ADD user',
awToolTip: 'Add existing user to organization',
actionClass: 'btn List-buttonSubmit',
ngClick: 'addUsers()'
}
};
url = data.related.users;
list.listTitle = listTitle;
list.basePath = url;
$scope.orgRelatedUrls = data.related;
generator.inject(list, { mode: 'edit', scope: $scope, cancelButton: true });
SearchInit({
scope: $scope,
set: 'users',
list: list,
url: url
});
PaginateInit({
scope: $scope,
list: list,
url: url
});
$scope.search(list.iterator);
});
$scope.addUsers = function () {
$compile("<add-users class='AddUsers'></add-users>")($scope);
};
$scope.editUser = function (id) {
$state.transitionTo('users.edit', {user_id: id});
};
$scope.deleteUser = function (id, name) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
var url = orgBase + $stateParams.organization_id + '/users/';
Rest.setUrl(url);
Rest.post({
id: id,
disassociate: true
}).success(function () {
$scope.search(list.iterator);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to remove the following user from this organization?</div><div class="Prompt-bodyTarget">' + name + '</div>',
action: action,
actionText: 'DELETE'
});
};
$scope.formCancel = function(){
$state.go('organizations');
};
}
];

View File

@ -1,100 +0,0 @@
export default ['$compile', '$scope', '$stateParams', '$state', 'Rest', 'UserList', 'InventoryList', 'JobTemplateList', 'TeamList', 'ProjectList', 'generateList', 'SearchInit', 'PaginateInit', function($compile, $scope, $stateParams, $state, Rest, UserList, InventoryList, JobTemplateList, TeamList, ProjectList, GenerateList, SearchInit, PaginateInit) {
var getList = function(mode) {
var list = {};
if (mode === 'users') {
list = _.cloneDeep(UserList);
list.emptyListText = "Please add items to this list";
list.actions.add.label = "Add a user to the organization";
list.actions.add.buttonContent = '&#43; ADD user';
list.actions.add.awToolTip = 'Add existing user to organization';
list.actions.add.ngClick = 'addUsers()';
} else if (mode === 'inventories') {
list = _.cloneDeep(InventoryList);
list.emptyListText = "List is empty";
delete list.actions.add;
} else if (mode === 'job_templates') {
list = _.cloneDeep(JobTemplateList);
list.emptyListText = "List is empty";
delete list.actions.add;
} else if (mode === 'teams') {
list = _.cloneDeep(TeamList);
list.emptyListText = "List is empty";
delete list.actions.add;
} else if (mode === 'projects') {
list = _.cloneDeep(ProjectList);
list.emptyListText = "List is empty";
delete list.actions.add;
} else if (mode === 'admins') {
list = _.cloneDeep(UserList);
list.emptyListText = "Please add items to this list";
list.actions.add.buttonContent = '&#43; ADD administrator';
list.actions.add.awToolTip = 'Add existing user to organization as administrator';
list.actions.add.ngClick = 'addUsers()';
}
return list;
};
var getUrl = function(mode, data) {
var url = "";
if (mode === 'users') {
url = data.related.users;
} else if (mode === 'inventories') {
url = data.related.inventories;
} else if (mode === 'job_templates') {
url = "/api/v1/job_templates/?project__organization=" + data.id;
} else if (mode === 'teams') {
url = data.related.teams;
} else if (mode === 'projects') {
url = data.related.projects;
} else if (mode === 'admins') {
url = data.related.admins;
}
return url;
};
Rest.setUrl("/api/v1/organizations/" + $stateParams.organization_id);
Rest.get()
.success(function (data) {
// include name of item in listTitle
var mode = $state.current.name.split(".")[1],
listTitle = data.name +
"<div class='List-titleLockup'></div>" +
mode.replace('_', ' '),
list,
url,
generator = GenerateList;
$scope.$parent.activeCard = parseInt($stateParams.organization_id);
$scope.$parent.activeMode = mode;
$scope.org_name = data.name;
$scope.org_id = data.id;
var listMode = (mode === 'admins') ? 'users' : mode;
list = getList(mode);
url = getUrl(mode, data);
list.listTitle = listTitle;
list.basePath = url;
$scope.orgRelatedUrls = data.related;
generator
.inject(list, { mode: 'edit', scope: $scope });
$scope.addUsers = function () {
$compile("<add-users class='AddUsers'></add-users>")($scope);
};
SearchInit({
scope: $scope,
set: listMode,
list: list,
url: url
});
PaginateInit({
scope: $scope,
list: list,
url: url
});
$scope.search(list.iterator);
});
}];

View File

@ -5,14 +5,19 @@
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import OrganizationsLinkout from './organizations-linkout.controller';
import OrganizationsAdmins from './controllers/organizations-admins.controller';
import OrganizationsInventories from './controllers/organizations-inventories.controller';
import OrganizationsJobTemplates from './controllers/organizations-job-templates.controller';
import OrganizationsProjects from './controllers/organizations-projects.controller';
import OrganizationsTeams from './controllers/organizations-teams.controller';
import OrganizationsUsers from './controllers/organizations-users.controller';
export default [
{
name: 'organizations.users',
route: '/:organization_id/users',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsUsers,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -20,9 +25,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "USERS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -34,7 +39,7 @@ export default [
name: 'organizations.teams',
route: '/:organization_id/teams',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsTeams,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -42,9 +47,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "TEAMS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -56,7 +61,7 @@ export default [
name: 'organizations.inventories',
route: '/:organization_id/inventories',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsInventories,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -64,9 +69,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "INVENTORIES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -78,7 +83,7 @@ export default [
name: 'organizations.projects',
route: '/:organization_id/projects',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsProjects,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -86,9 +91,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "PROJECTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -100,7 +105,7 @@ export default [
name: 'organizations.job_templates',
route: '/:organization_id/job_templates',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsJobTemplates,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -108,9 +113,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "JOB TEMPLATES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
@ -122,7 +127,7 @@ export default [
name: 'organizations.admins',
route: '/:organization_id/admins',
templateUrl: templateUrl('organizations/linkout/organizations-linkout'),
controller: OrganizationsLinkout,
controller: OrganizationsAdmins,
data: {
activityStream: true,
activityStreamTarget: 'organization'
@ -130,9 +135,9 @@ export default [
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "setup";
return "organizations.edit";
},
label: "ORGANIZATIONS"
label: "ADMINS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {

View File

@ -59,7 +59,7 @@
</div>
</div>
<div class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
<div class="col-lg-7 col-md-12 col-sm-12 col-xs-12">
<div class="TagSearch-tagSection">
<div class="TagSearch-flexContainer">
<div class="TagSearch-tagContainer"

View File

@ -99,9 +99,9 @@
import {templateUrl} from '../../shared/template-url/template-url.factory';
export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'getSearchHtml',
'Column', 'DropDown', 'NavigationLink', 'SelectIcon',
'Column', 'DropDown', 'NavigationLink', 'SelectIcon', 'ActionButton',
function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, getSearchHtml, Column, DropDown, NavigationLink,
SelectIcon) {
SelectIcon, ActionButton) {
return {
setList: function (list) {
@ -363,10 +363,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
html += "<div class=\"List-well\">\n";
}
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
html += "<div class=\"List-noItems\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == false && " + list.iterator + "_total_rows < 1\">";
html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
html += "</div>";
html += (list.searchRowActions) ? "<div class='row'><div class=\"col-lg-8 col-md-8 col-sm-8 col-xs-12\">" : "";
if (options.showSearch=== undefined || options.showSearch === true) {
var tagSearch = getSearchHtml
.inject(getSearchHtml.getList(list),
@ -381,12 +378,35 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
${tagSearch}
</div>
`;
}
if(list.searchRowActions) {
html += "</div><div class='col-lg-4 col-md-4 col-sm-4 col-xs-12'>";
var actionButtons = "";
Object.keys(list.searchRowActions || {})
.forEach(act => {
actionButtons += ActionButton(list.searchRowActions[act]);
});
html += `
<div class=\"list-actions\">
${actionButtons}
</div>
`;
html += "</div></div>";
}
if (options.showSearch=== undefined || options.showSearch === true) {
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
html += "<div class=\"row\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == true && " + list.name + ".length == 0\">\n";
html += "<div class=\"col-lg-12 List-searchNoResults\">No records matched your search.</div>\n";
html += "</div>\n";
}
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
html += "<div class=\"List-noItems\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == false && " + list.iterator + "_total_rows < 1\">";
html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
html += "</div>";
// Add a title and optionally a close button (used on Inventory->Groups)
if (options.mode !== 'lookup' && list.showTitle) {
html += "<div class=\"form-title\">";