diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 862bdc8b1d..9db335cdfa 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -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%; @@ -1970,7 +1971,6 @@ tr td button i { } .list-actions { - margin-bottom: 20px; text-align: left; } diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index 0da16a64ee..4fd209c9d1 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -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!', diff --git a/awx/ui/client/src/organizations/edit/organizations-edit.route.js b/awx/ui/client/src/organizations/edit/organizations-edit.route.js index be90b6b282..c0723272cd 100644 --- a/awx/ui/client/src/organizations/edit/organizations-edit.route.js +++ b/awx/ui/client/src/organizations/edit/organizations-edit.route.js @@ -19,6 +19,6 @@ export default { }, ncyBreadcrumb: { parent: "organizations", - label: "{{name}}" + label: "{{organization_name}}" } }; diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html index 708f7925b6..ca166a2bb9 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.partial.html @@ -5,7 +5,7 @@
-
{{ $parent.org_name }}
Add {{ addType }} +
{{ $parent.organization_name }}
Add {{ addType }}
diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js new file mode 100644 index 0000000000..e81e01195e --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-admins.controller.js @@ -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 + "
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: '+ 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("")($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: '
Are you sure you want to remove the following administrator from this organization?
' + name + '
', + action: action, + actionText: 'DELETE' + }); + }; + + $scope.formCancel = function(){ + $state.go('organizations'); + }; + + } +]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js new file mode 100644 index 0000000000..d7cf318f8c --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js @@ -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 + "
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 Inventories 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
. 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 = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + html += "\n"; + + data.results.forEach(function(row) { + html += "\n"; + html += "\n"; + html += ""; + html += ""; + html += "\n"; + }); + html += "\n"; + html += "
StatusFinishedName
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + ellipsis(row.name) + "
\n"; + } + else { + html = "

No recent job data available for this inventory.

\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 = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + data.results.forEach( function(row) { + if (row.related.last_update) { + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + } + else { + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + } + }); + html += "\n"; + html += "
StatusLast SyncGroup
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + ellipsis(row.summary_fields.group.name) + "
NA" + ellipsis(row.summary_fields.group.name) + "
\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'); + }; + + } +]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js new file mode 100644 index 0000000000..7f13f997b5 --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js @@ -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 + "
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 Job Templates 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'); + }; + + } +]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js new file mode 100644 index 0000000000..94e84954af --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -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 + "
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 Projects 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(//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 Refresh 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 Refresh ' + + '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'); + }; + + } +]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js new file mode 100644 index 0000000000..442eb59e9e --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js @@ -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 + "
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 Teams 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'); + }; + } +]; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js new file mode 100644 index 0000000000..f3c868b406 --- /dev/null +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-users.controller.js @@ -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 + "
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: '+ 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("")($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: '
Are you sure you want to remove the following user from this organization?
' + name + '
', + action: action, + actionText: 'DELETE' + }); + }; + + $scope.formCancel = function(){ + $state.go('organizations'); + }; + + } +]; diff --git a/awx/ui/client/src/organizations/linkout/organizations-linkout.controller.js b/awx/ui/client/src/organizations/linkout/organizations-linkout.controller.js deleted file mode 100644 index ea5ae1cd22..0000000000 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.controller.js +++ /dev/null @@ -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 = '+ 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 = '+ 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 + - "
" + - 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("")($scope); - }; - - SearchInit({ - scope: $scope, - set: listMode, - list: list, - url: url - }); - PaginateInit({ - scope: $scope, - list: list, - url: url - }); - $scope.search(list.iterator); - }); -}]; diff --git a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js index 448a635fc4..13d51cc68f 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -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) { diff --git a/awx/ui/client/src/search/tagSearch.partial.html b/awx/ui/client/src/search/tagSearch.partial.html index 3f1108018d..59fbf16c8a 100644 --- a/awx/ui/client/src/search/tagSearch.partial.html +++ b/awx/ui/client/src/search/tagSearch.partial.html @@ -58,7 +58,7 @@
-
+
\n"; } - // Show the "no items" box when loading is done and the user isn't actively searching and there are no results - html += "
"; - html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; - html += "
"; + html += (list.searchRowActions) ? "
" : ""; 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}
`; + } + if(list.searchRowActions) { + html += "
"; + + var actionButtons = ""; + Object.keys(list.searchRowActions || {}) + .forEach(act => { + actionButtons += ActionButton(list.searchRowActions[act]); + }); + html += ` +
+ ${actionButtons} +
+ `; + html += "
"; + } + + 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 += "
\n"; html += "
No records matched your search.
\n"; html += "
\n"; } + // Show the "no items" box when loading is done and the user isn't actively searching and there are no results + html += "
"; + html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; + html += "
"; + // Add a title and optionally a close button (used on Inventory->Groups) if (options.mode !== 'lookup' && list.showTitle) { html += "
";