From d24ab43c9941223934438710eb447370b2d25433 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 14 Mar 2016 16:13:01 -0400 Subject: [PATCH] Set up inventory module and split apart controllers/inventory.js into more manageable chunks. --- awx/ui/client/src/app.js | 40 -- .../src/inventory/inventory-add.controller.js | 98 ++++ .../inventory/inventory-edit.controller.js | 332 +++++++++++ .../inventory/inventory-list.controller.js | 371 ++++++++++++ .../inventory/inventory-manage.controller.js | 532 ++++++++++++++++++ awx/ui/client/src/inventory/main.js | 10 + 6 files changed, 1343 insertions(+), 40 deletions(-) create mode 100644 awx/ui/client/src/inventory/inventory-add.controller.js create mode 100644 awx/ui/client/src/inventory/inventory-edit.controller.js create mode 100644 awx/ui/client/src/inventory/inventory-list.controller.js create mode 100644 awx/ui/client/src/inventory/inventory-manage.controller.js create mode 100644 awx/ui/client/src/inventory/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 55753b9f40..aabeec0f4c 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -373,7 +373,6 @@ var tower = angular.module('Tower', [ } }). -<<<<<<< 3e32787490a4faf3899b1a5d125475e73521ef35 state('inventories', { url: '/inventories', templateUrl: urlPrefix + 'partials/inventories.html', @@ -384,22 +383,6 @@ var tower = angular.module('Tower', [ }, ncyBreadcrumb: { label: "INVENTORIES" -======= - state('organizations', { - url: '/organizations', - templateUrl: urlPrefix + 'partials/organizations.html', - controller: OrganizationsList, - data: { - activityStream: true, - activityStreamTarget: 'organization' - }, - ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "setup"; - }, - label: "ORGANIZATIONS" ->>>>>>> Further modularization. }, resolve: { features: ['FeaturesService', function(FeaturesService) { @@ -408,7 +391,6 @@ var tower = angular.module('Tower', [ } }). -<<<<<<< 3e32787490a4faf3899b1a5d125475e73521ef35 state('inventories.add', { url: '/add', templateUrl: urlPrefix + 'partials/inventories.html', @@ -416,15 +398,6 @@ var tower = angular.module('Tower', [ ncyBreadcrumb: { parent: "inventories", label: "CREATE INVENTORY" -======= - state('organizations.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/organizations.crud.html', - controller: OrganizationsAdd, - ncyBreadcrumb: { - parent: "organizations", - label: "CREATE ORGANIZATION" ->>>>>>> Further modularization. }, resolve: { features: ['FeaturesService', function(FeaturesService) { @@ -433,7 +406,6 @@ var tower = angular.module('Tower', [ } }). -<<<<<<< 3e32787490a4faf3899b1a5d125475e73521ef35 state('inventories.edit', { url: '/:inventory_id', templateUrl: urlPrefix + 'partials/inventories.html', @@ -456,18 +428,6 @@ var tower = angular.module('Tower', [ activityStream: true, activityStreamTarget: 'inventory', activityStreamId: 'inventory_id' -======= - state('organizations.edit', { - url: '/:organization_id', - templateUrl: urlPrefix + 'partials/organizations.crud.html', - controller: OrganizationsEdit, - data: { - activityStreamId: 'organization_id' - }, - ncyBreadcrumb: { - parent: "organizations", - label: "{{name}}" ->>>>>>> Further modularization. }, resolve: { features: ['FeaturesService', function(FeaturesService) { diff --git a/awx/ui/client/src/inventory/inventory-add.controller.js b/awx/ui/client/src/inventory/inventory-add.controller.js new file mode 100644 index 0000000000..a0903f4fab --- /dev/null +++ b/awx/ui/client/src/inventory/inventory-add.controller.js @@ -0,0 +1,98 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +import '../job-templates/main'; + +function InventoriesAdd($scope, $rootScope, $compile, $location, $log, + $stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, + ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit, + PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON, + $state) { + + ClearScope(); + + // Inject dynamic view + var defaultUrl = GetBasePath('inventory'), + form = InventoryForm(), + generator = GenerateForm; + + form.well = true; + form.formLabelSize = null; + form.formFieldSize = null; + + generator.inject(form, { mode: 'add', related: false, scope: $scope }); + + generator.reset(); + + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'variables', + parse_variable: 'parseType', + field_id: 'inventory_variables' + }); + + LookUpInit({ + scope: $scope, + form: form, + current_item: ($stateParams.organization_id) ? $stateParams.organization_id : null, + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + + // Save + $scope.formSave = function () { + generator.clearApiErrors(); + Wait('start'); + try { + var fld, json_data, data; + + json_data = ToJSON($scope.parseType, $scope.variables, true); + + data = {}; + for (fld in form.fields) { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + + Rest.setUrl(defaultUrl); + Rest.post(data) + .success(function (data) { + var inventory_id = data.id; + Wait('stop'); + $location.path('/inventories/' + inventory_id + '/manage'); + }) + .error(function (data, status) { + ProcessErrors( $scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new inventory. Post returned status: ' + status }); + }); + } catch (err) { + Wait('stop'); + Alert("Error", "Error parsing inventory variables. Parser returned: " + err); + } + + }; + + $scope.formCancel = function () { + $state.transitionTo('inventories'); + }; +} + +export default['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert', + 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList', + 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', + 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state', InventoriesAdd] diff --git a/awx/ui/client/src/inventory/inventory-edit.controller.js b/awx/ui/client/src/inventory/inventory-edit.controller.js new file mode 100644 index 0000000000..dd45a566ac --- /dev/null +++ b/awx/ui/client/src/inventory/inventory-edit.controller.js @@ -0,0 +1,332 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +import '../job-templates/main'; + +function InventoriesEdit($scope, $rootScope, $compile, $location, + $log, $stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, + ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit, + PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON, + ParseVariableString, RelatedSearchInit, RelatedPaginateInit, + Prompt, PlaybookRun, CreateDialog, deleteJobTemplate, $state) { + + ClearScope(); + + // Inject dynamic view + var defaultUrl = GetBasePath('inventory'), + form = InventoryForm(), + generator = GenerateForm, + inventory_id = $stateParams.inventory_id, + master = {}, + fld, json_data, data, + relatedSets = {}; + + form.well = true; + form.formLabelSize = null; + form.formFieldSize = null; + $scope.inventory_id = inventory_id; + generator.inject(form, { mode: 'edit', related: true, scope: $scope }); + + generator.reset(); + + + // After the project is loaded, retrieve each related set + if ($scope.inventoryLoadedRemove) { + $scope.inventoryLoadedRemove(); + } + $scope.projectLoadedRemove = $scope.$on('inventoryLoaded', function () { + var set; + for (set in relatedSets) { + $scope.search(relatedSets[set].iterator); + } + }); + + Wait('start'); + Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); + Rest.get() + .success(function (data) { + var fld; + for (fld in form.fields) { + if (fld === 'variables') { + $scope.variables = ParseVariableString(data.variables); + master.variables = $scope.variables; + } else if (fld === 'inventory_name') { + $scope[fld] = data.name; + master[fld] = $scope[fld]; + } else if (fld === 'inventory_description') { + $scope[fld] = data.description; + master[fld] = $scope[fld]; + } else if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = $scope[fld]; + } + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + relatedSets = form.relatedSets(data.related); + + // Initialize related search functions. Doing it here to make sure relatedSets object is populated. + RelatedSearchInit({ + scope: $scope, + form: form, + relatedSets: relatedSets + }); + RelatedPaginateInit({ + scope: $scope, + relatedSets: relatedSets + }); + + Wait('stop'); + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'variables', + parse_variable: 'parseType', + field_id: 'inventory_variables' + }); + LookUpInit({ + scope: $scope, + form: form, + current_item: $scope.organization, + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + $scope.$emit('inventoryLoaded'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status }); + }); + // Save + $scope.formSave = function () { + Wait('start'); + + // Make sure we have valid variable data + json_data = ToJSON($scope.parseType, $scope.variables); + + data = {}; + for (fld in form.fields) { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + + Rest.setUrl(defaultUrl + inventory_id + '/'); + Rest.put(data) + .success(function () { + Wait('stop'); + $location.path('/inventories/'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to update inventory. PUT returned status: ' + status }); + }); + }; + + $scope.manageInventory = function(){ + $location.path($location.path() + '/manage'); + }; + + $scope.formCancel = function () { + $state.transitionTo('inventories'); + }; + + $scope.addScanJob = function(){ + $location.path($location.path()+'/job_templates/add'); + }; + + $scope.launchScanJob = function(){ + PlaybookRun({ scope: $scope, id: this.scan_job_template.id }); + }; + + $scope.scheduleScanJob = function(){ + $location.path('/job_templates/'+this.scan_job_template.id+'/schedules'); + }; + + $scope.editScanJob = function(){ + $location.path($location.path()+'/job_templates/'+this.scan_job_template.id); + }; + + $scope.copyScanJobTemplate = function(){ + var id = this.scan_job_template.id, + name = this.scan_job_template.name, + element, + buttons = [{ + "label": "Cancel", + "onClick": function() { + $(this).dialog('close'); + }, + "icon": "fa-times", + "class": "btn btn-default", + "id": "copy-close-button" + },{ + "label": "Copy", + "onClick": function() { + copyAction(); + }, + "icon": "fa-copy", + "class": "btn btn-primary", + "id": "job-copy-button" + }], + copyAction = function () { + // retrieve the copy of the job template object from the api, then overwrite the name and throw away the id + Wait('start'); + var url = GetBasePath('job_templates')+id; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + data.name = $scope.new_copy_name; + delete data.id; + $scope.$emit('GoToCopy', data); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + + CreateDialog({ + id: 'copy-job-modal' , + title: "Copy", + scope: $scope, + buttons: buttons, + width: 500, + height: 300, + minWidth: 200, + callback: 'CopyDialogReady' + }); + + $('#job_name').text(name); + $('#copy-job-modal').show(); + + + if ($scope.removeCopyDialogReady) { + $scope.removeCopyDialogReady(); + } + $scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() { + //clear any old remaining text + $scope.new_copy_name = "" ; + $scope.copy_form.$setPristine(); + $('#copy-job-modal').dialog('open'); + $('#job-copy-button').attr('ng-disabled', "!copy_form.$valid"); + element = angular.element(document.getElementById('job-copy-button')); + $compile(element)($scope); + + }); + + if ($scope.removeGoToCopy) { + $scope.removeGoToCopy(); + } + $scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) { + var url = GetBasePath('job_templates'), + old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ; + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + if(data.survey_enabled===true){ + $scope.$emit("CopySurvey", data, old_survey_url); + } + else { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/job_templates/' + data.id); + } + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }); + + if ($scope.removeCopySurvey) { + $scope.removeCopySurvey(); + } + $scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) { + // var url = data.related.survey_spec; + Rest.setUrl(old_url); + Rest.get() + .success(function (survey_data) { + + Rest.setUrl(new_data.related.survey_spec); + Rest.post(survey_data) + .success(function () { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/job_templates/' + new_data.id); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status }); + }); + + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status }); + }); + + }); + + }; + + $scope.deleteScanJob = function () { + var id = this.scan_job_template.id , + action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + deleteJobTemplate(id) + .success(function () { + $('#prompt-modal').modal('hide'); + $scope.search(form.related.scan_job_templates.iterator); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'DELETE returned status: ' + status }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to delete the job template below?
' + this.scan_job_template.name + '
', + action: action, + actionText: 'DELETE' + }); + + }; + +} + +export default ['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert', + 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList', + 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', + 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', + 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', + 'PlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state', + InventoriesEdit, +]; diff --git a/awx/ui/client/src/inventory/inventory-list.controller.js b/awx/ui/client/src/inventory/inventory-list.controller.js new file mode 100644 index 0000000000..98f02df918 --- /dev/null +++ b/awx/ui/client/src/inventory/inventory-list.controller.js @@ -0,0 +1,371 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +import '../job-templates/main'; + +function InventoriesList($scope, $rootScope, $location, $log, + $stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList, + generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, + ClearScope, ProcessErrors, GetBasePath, Wait, + EditInventoryProperties, Find, Empty, $state) { + + var list = InventoryList, + defaultUrl = GetBasePath('inventory'), + view = generateList, + paths = $location.path().replace(/^\//, '').split('/'), + mode = (paths[0] === 'inventories') ? 'edit' : 'select'; + + 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'); + } + + view.inject(InventoryList, { mode: mode, scope: $scope }); + $rootScope.flashMessage = null; + + SearchInit({ + scope: $scope, + set: 'inventories', + list: list, + url: defaultUrl + }); + + PaginateInit({ + scope: $scope, + list: list, + url: defaultUrl + }); + + 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); + + 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.editInventoryProperties = function (inventory_id) { + EditInventoryProperties({ scope: $scope, inventory_id: inventory_id }); + }; + + $scope.addInventory = function () { + $state.go('inventories.add'); + }; + + $scope.editInventory = function (id) { + $state.go('inventories.edit', {inventory_id: id}); + }; + + $scope.manageInventory = function(id){ + $location.path($location.path() + '/' + id + '/manage'); + }; + + $scope.deleteInventory = function (id, name) { + + var action = function () { + var url = defaultUrl + id + '/'; + Wait('start'); + $('#prompt-modal').modal('hide'); + Rest.setUrl(url); + Rest.destroy() + .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 delete the inventory below?
' + $filter('sanitize')(name) + '
', + action: action, + actionText: 'DELETE' + }); + }; + + $scope.lookupOrganization = function (organization_id) { + Rest.setUrl(GetBasePath('organizations') + organization_id + '/'); + Rest.get() + .success(function (data) { + return data.name; + }); + }; + + + // Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status + $scope.viewJobs = function (id) { + $location.url('/jobs/?inventory__int=' + id); + }; + + $scope.viewFailedJobs = function (id) { + $location.url('/jobs/?inventory__int=' + id + '&status=failed'); + }; +} + +export default ['$scope', '$rootScope', '$location', '$log', + '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', + 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', + 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', + 'EditInventoryProperties', 'Find', 'Empty', '$state', InventoriesList]; diff --git a/awx/ui/client/src/inventory/inventory-manage.controller.js b/awx/ui/client/src/inventory/inventory-manage.controller.js new file mode 100644 index 0000000000..651547894e --- /dev/null +++ b/awx/ui/client/src/inventory/inventory-manage.controller.js @@ -0,0 +1,532 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +import '../job-templates/main'; + +function InventoriesManage($log, $scope, $rootScope, $location, + $state, $compile, generateList, ClearScope, Empty, Wait, Rest, Alert, + GetBasePath, ProcessErrors, InventoryGroups, + InjectHosts, Find, HostsReload, SearchInit, PaginateInit, GetSyncStatusMsg, + GetHostsStatusMsg, GroupsEdit, InventoryUpdate, GroupsCancelUpdate, + ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete, + EditInventoryProperties, ToggleHostEnabled, ShowJobSummary, + InventoryGroupsHelp, HelpDialog, + GroupsCopy, HostsCopy, $stateParams) { + + var PreviousSearchParams, + url, + hostScope = $scope.$new(); + + ClearScope(); + + // TODO: only display adhoc button if the user has permission to use it. + // TODO: figure out how to get the action-list partial to update so that + // the tooltip can be changed based off things being selected or not. + $scope.adhocButtonTipContents = "Launch adhoc command for the inventory"; + + // watcher for the group list checkbox changes + $scope.$on('multiSelectList.selectionChanged', function(e, selection) { + if (selection.length > 0) { + $scope.groupsSelected = true; + // $scope.adhocButtonTipContents = "Launch adhoc command for the " + // + "selected groups and hosts."; + } else { + $scope.groupsSelected = false; + // $scope.adhocButtonTipContents = "Launch adhoc command for the " + // + "inventory."; + } + $scope.groupsSelectedItems = selection.selectedItems; + }); + + // watcher for the host list checkbox changes + hostScope.$on('multiSelectList.selectionChanged', function(e, selection) { + // you need this so that the event doesn't bubble to the watcher above + // for the host list + e.stopPropagation(); + if (selection.length === 0) { + $scope.hostsSelected = false; + } else if (selection.length === 1) { + $scope.systemTrackingTooltip = "Compare host over time"; + $scope.hostsSelected = true; + $scope.systemTrackingDisabled = false; + } else if (selection.length === 2) { + $scope.systemTrackingTooltip = "Compare hosts against each other"; + $scope.hostsSelected = true; + $scope.systemTrackingDisabled = false; + } else { + $scope.hostsSelected = true; + $scope.systemTrackingDisabled = true; + } + $scope.hostsSelectedItems = selection.selectedItems; + }); + + $scope.systemTracking = function() { + var hostIds = _.map($scope.hostsSelectedItems, function(x){ + return x.id; + }); + $state.transitionTo('systemTracking', + { inventory: $scope.inventory, + inventoryId: $scope.inventory.id, + hosts: $scope.hostsSelectedItems, + hostIds: hostIds + }); + }; + + // populates host patterns based on selected hosts/groups + $scope.populateAdhocForm = function() { + var host_patterns = "all"; + if ($scope.hostsSelected || $scope.groupsSelected) { + var allSelectedItems = []; + if ($scope.groupsSelectedItems) { + allSelectedItems = allSelectedItems.concat($scope.groupsSelectedItems); + } + if ($scope.hostsSelectedItems) { + allSelectedItems = allSelectedItems.concat($scope.hostsSelectedItems); + } + if (allSelectedItems) { + host_patterns = _.pluck(allSelectedItems, "name").join(":"); + } + } + $rootScope.hostPatterns = host_patterns; + $state.go('inventoryManage.adhoc'); + }; + + $scope.refreshHostsOnGroupRefresh = false; + $scope.selected_group_id = null; + + Wait('start'); + + + if ($scope.removeHostReloadComplete) { + $scope.removeHostReloadComplete(); + } + $scope.removeHostReloadComplete = $scope.$on('HostReloadComplete', function() { + if ($scope.initial_height) { + var host_height = $('#hosts-container .well').height(), + group_height = $('#group-list-container .well').height(), + new_height; + + if (host_height > group_height) { + new_height = host_height - (host_height - group_height); + } + else if (host_height < group_height) { + new_height = host_height + (group_height - host_height); + } + if (new_height) { + $('#hosts-container .well').height(new_height); + } + $scope.initial_height = null; + } + }); + + if ($scope.removeRowCountReady) { + $scope.removeRowCountReady(); + } + $scope.removeRowCountReady = $scope.$on('RowCountReady', function(e, rows) { + // Add hosts view + $scope.show_failures = false; + InjectHosts({ + group_scope: $scope, + host_scope: hostScope, + inventory_id: $scope.inventory.id, + tree_id: null, + group_id: null, + pageSize: rows + }); + + SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: $scope.inventory.related.root_groups }); + PaginateInit({ scope: $scope, list: InventoryGroups , url: $scope.inventory.related.root_groups, pageSize: rows }); + $scope.search(InventoryGroups.iterator, null, true); + }); + + if ($scope.removeInventoryLoaded) { + $scope.removeInventoryLoaded(); + } + $scope.removeInventoryLoaded = $scope.$on('InventoryLoaded', function() { + var rows; + + // Add groups view + generateList.inject(InventoryGroups, { + mode: 'edit', + id: 'group-list-container', + searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12', + scope: $scope + }); + + rows = 20; + hostScope.host_page_size = rows; + $scope.group_page_size = rows; + + $scope.show_failures = false; + InjectHosts({ + group_scope: $scope, + host_scope: hostScope, + inventory_id: $scope.inventory.id, + tree_id: null, + group_id: null, + pageSize: rows + }); + + // Load data + SearchInit({ + scope: $scope, + set: 'groups', + list: InventoryGroups, + url: $scope.inventory.related.root_groups + }); + + PaginateInit({ + scope: $scope, + list: InventoryGroups , + url: $scope.inventory.related.root_groups, + pageSize: rows + }); + + $scope.search(InventoryGroups.iterator, null, true); + + $scope.$emit('WatchUpdateStatus'); // init socket io conneciton and start watching for status updates + }); + + if ($scope.removePostRefresh) { + $scope.removePostRefresh(); + } + $scope.removePostRefresh = $scope.$on('PostRefresh', function(e, set) { + if (set === 'groups') { + $scope.groups.forEach( function(group, idx) { + var stat, hosts_status; + stat = GetSyncStatusMsg({ + status: group.summary_fields.inventory_source.status, + has_inventory_sources: group.has_inventory_sources, + source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) + }); // from helpers/Groups.js + $scope.groups[idx].status_class = stat['class']; + $scope.groups[idx].status_tooltip = stat.tooltip; + $scope.groups[idx].launch_tooltip = stat.launch_tip; + $scope.groups[idx].launch_class = stat.launch_class; + hosts_status = GetHostsStatusMsg({ + active_failures: group.hosts_with_active_failures, + total_hosts: group.total_hosts, + inventory_id: $scope.inventory.id, + group_id: group.id + }); // from helpers/Groups.js + $scope.groups[idx].hosts_status_tip = hosts_status.tooltip; + $scope.groups[idx].show_failures = hosts_status.failures; + $scope.groups[idx].hosts_status_class = hosts_status['class']; + + $scope.groups[idx].source = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null; + $scope.groups[idx].status = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.status : null; + + }); + if ($scope.refreshHostsOnGroupRefresh) { + $scope.refreshHostsOnGroupRefresh = false; + HostsReload({ + scope: hostScope, + group_id: $scope.selected_group_id, + inventory_id: $scope.inventory.id, + pageSize: hostScope.host_page_size + }); + } + else { + Wait('stop'); + } + } + }); + + // Load Inventory + url = GetBasePath('inventory') + $stateParams.inventory_id + '/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + $scope.inventory = data; + $scope.$emit('InventoryLoaded'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory: ' + $stateParams.inventory_id + + ' GET returned status: ' + status }); + }); + + // start watching for real-time updates + if ($rootScope.removeWatchUpdateStatus) { + $rootScope.removeWatchUpdateStatus(); + } + $rootScope.removeWatchUpdateStatus = $rootScope.$on('JobStatusChange-inventory', function(e, data) { + var stat, group; + if (data.group_id) { + group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + if (data.status === "failed" || data.status === "successful") { + if (data.group_id === $scope.selected_group_id || group) { + // job completed, fefresh all groups + $log.debug('Update completed. Refreshing the tree.'); + $scope.refreshGroups(); + } + } + else if (group) { + // incremental update, just update + $log.debug('Status of group: ' + data.group_id + ' changed to: ' + data.status); + stat = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + $log.debug('changing tooltip to: ' + stat.tooltip); + group.status = data.status; + group.status_class = stat['class']; + group.status_tooltip = stat.tooltip; + group.launch_tooltip = stat.launch_tip; + group.launch_class = stat.launch_class; + } + } + }); + + // Load group on selection + function loadGroups(url) { + SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: url }); + PaginateInit({ scope: $scope, list: InventoryGroups , url: url, pageSize: $scope.group_page_size }); + $scope.search(InventoryGroups.iterator, null, true, false, true); + } + + $scope.refreshHosts = function() { + HostsReload({ + scope: hostScope, + group_id: $scope.selected_group_id, + inventory_id: $scope.inventory.id, + pageSize: hostScope.host_page_size + }); + }; + + $scope.refreshGroups = function() { + $scope.refreshHostsOnGroupRefresh = true; + $scope.search(InventoryGroups.iterator, null, true, false, true); + }; + + $scope.restoreSearch = function() { + // Restore search params and related stuff, plus refresh + // groups and hosts lists + SearchInit({ + scope: $scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + $scope.refreshHostsOnGroupRefresh = true; + $scope.search(InventoryGroups.iterator, null, true, false, true); + }; + + $scope.groupSelect = function(id) { + var groups = [], group = Find({ list: $scope.groups, key: 'id', val: id }); + if($state.params.groups){ + groups.push($state.params.groups); + } + groups.push(group.id); + groups = groups.join(); + $state.transitionTo('inventoryManage', {inventory_id: $state.params.inventory_id, groups: groups}, { notify: false }); + loadGroups(group.related.children, group.id); + }; + + $scope.createGroup = function () { + PreviousSearchParams = Store('group_current_search_params'); + GroupsEdit({ + scope: $scope, + inventory_id: $scope.inventory.id, + group_id: $scope.selected_group_id, + mode: 'add' + }); + }; + + $scope.editGroup = function (id) { + PreviousSearchParams = Store('group_current_search_params'); + GroupsEdit({ + scope: $scope, + inventory_id: $scope.inventory.id, + group_id: id, + mode: 'edit' + }); + }; + + // Launch inventory sync + $scope.updateGroup = function (id) { + var group = Find({ list: $scope.groups, key: 'id', val: id }); + if (group) { + if (Empty(group.source)) { + // if no source, do nothing. + } else if (group.status === 'updating') { + Alert('Update in Progress', 'The inventory update process is currently running for group ' + + group.name + ' Click the button to monitor the status.', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + InventoryUpdate({ + scope: $scope, + url: data.related.update, + group_name: data.summary_fields.group.name, + group_source: data.source, + group_id: group.id, + }); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + + group.related.inventory_source + ' GET returned status: ' + status }); + }); + } + } + }; + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + + $scope.copyGroup = function(id) { + PreviousSearchParams = Store('group_current_search_params'); + GroupsCopy({ + scope: $scope, + group_id: id + }); + }; + + $scope.deleteGroup = function (id) { + GroupsDelete({ + scope: $scope, + group_id: id, + inventory_id: $scope.inventory.id + }); + }; + + $scope.editInventoryProperties = function () { + // EditInventoryProperties({ scope: $scope, inventory_id: $scope.inventory.id }); + $location.path('/inventories/' + $scope.inventory.id + '/'); + }; + + hostScope.createHost = function () { + HostsEdit({ + host_scope: hostScope, + group_scope: $scope, + mode: 'add', + host_id: null, + selected_group_id: $scope.selected_group_id, + inventory_id: $scope.inventory.id + }); + }; + + hostScope.editHost = function (host_id) { + HostsEdit({ + host_scope: hostScope, + group_scope: $scope, + mode: 'edit', + host_id: host_id, + inventory_id: $scope.inventory.id + }); + }; + + hostScope.deleteHost = function (host_id, host_name) { + HostsDelete({ + parent_scope: $scope, + host_scope: hostScope, + host_id: host_id, + host_name: host_name + }); + }; + + hostScope.copyHost = function(id) { + PreviousSearchParams = Store('group_current_search_params'); + HostsCopy({ + group_scope: $scope, + host_scope: hostScope, + host_id: id + }); + }; + + /*hostScope.restoreSearch = function() { + SearchInit({ + scope: hostScope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + hostScope.search('host'); + };*/ + + hostScope.toggleHostEnabled = function (host_id, external_source) { + ToggleHostEnabled({ + parent_scope: $scope, + host_scope: hostScope, + host_id: host_id, + external_source: external_source + }); + }; + + hostScope.showJobSummary = function (job_id) { + ShowJobSummary({ + job_id: job_id + }); + }; + + $scope.showGroupHelp = function (params) { + var opts = { + defn: InventoryGroupsHelp + }; + if (params) { + opts.autoShow = params.autoShow || false; + } + HelpDialog(opts); + } +; + $scope.showHosts = function (group_id, show_failures) { + // Clicked on group + if (group_id !== null) { + Wait('start'); + hostScope.show_failures = show_failures; + $scope.groupSelect(group_id); + hostScope.hosts = []; + $scope.show_failures = show_failures; // turn on failed hosts + // filter in hosts view + } else { + Wait('stop'); + } + }; + + if ($scope.removeGroupDeleteCompleted) { + $scope.removeGroupDeleteCompleted(); + } + $scope.removeGroupDeleteCompleted = $scope.$on('GroupDeleteCompleted', + function() { + $scope.refreshGroups(); + } + ); +} + +export default [ + '$log', '$scope', '$rootScope', '$location', + '$state', '$compile', 'generateList', 'ClearScope', 'Empty', 'Wait', + 'Rest', 'Alert', 'GetBasePath', 'ProcessErrors', + 'InventoryGroups', 'InjectHosts', 'Find', 'HostsReload', + 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', + 'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus', + 'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete', + 'EditInventoryProperties', 'ToggleHostEnabled', 'ShowJobSummary', + 'InventoryGroupsHelp', 'HelpDialog', 'GroupsCopy', + 'HostsCopy', '$stateParams', InventoriesManage, +]; diff --git a/awx/ui/client/src/inventory/main.js b/awx/ui/client/src/inventory/main.js new file mode 100644 index 0000000000..0689a2d726 --- /dev/null +++ b/awx/ui/client/src/inventory/main.js @@ -0,0 +1,10 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +export default +angular.module('inventory', [ +])