From 0fa9aa6bcb02d695a66e11c4096cda3814c3174b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 20 Apr 2017 22:54:43 -0700 Subject: [PATCH] Adding nested-groups (related tab) and completed jobs to inventories --- ...-inventory-completed-jobs-state.factory.js | 80 +++++++ .../completed_jobs/completed_jobs.list.js | 88 +++++++ .../src/inventories/completed_jobs/main.js | 13 + .../src/inventories/groups/groups.form.js | 144 ++++++----- .../src/inventories/groups/groups.list.js | 10 +- awx/ui/client/src/inventories/groups/main.js | 6 +- .../inventories/groups/nested-groups/main.js | 17 ++ .../nested-groups-list.controller.js | 224 ++++++++++++++++++ .../nested-groups/nested-groups.form.js | 98 ++++++++ .../nested-groups/nested-groups.list.js | 146 ++++++++++++ .../client/src/inventories/inventory.form.js | 59 ++--- awx/ui/client/src/inventories/main.js | 2 + .../src/shared/stateDefinitions.factory.js | 2 + 13 files changed, 779 insertions(+), 110 deletions(-) create mode 100644 awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js create mode 100644 awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js create mode 100644 awx/ui/client/src/inventories/completed_jobs/main.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/main.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js diff --git a/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js b/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js new file mode 100644 index 0000000000..df06c8ede9 --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js @@ -0,0 +1,80 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import JobsListController from '../../jobs/jobs-list.controller'; +export default ['InventoryCompletedJobsList', '$stateExtender', 'templateUrl', '$injector', + function(InventoryCompletedJobsList, $stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + // searchPrefix: `${list.iterator}`, + name: `${formStateDefinition.name}.${list.iterator}s`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + completed_job_search: { + value: { + or__job__inventory: '', + or__adhoccommand__inventory: '', + or__inventoryupdate__inventory_source__inventory: '' + }, + squash: '' + } + }, + views: { + 'related': { + templateProvider: function(FormDefinition, GenerateForm) { + let html = GenerateForm.buildCollection({ + mode: 'edit', + related: `${list.iterator}s`, + form: typeof(FormDefinition) === 'function' ? + FormDefinition() : FormDefinition + }); + return html; + }, + controller: JobsListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + + $stateParams[`${list.iterator}_search`].or__job__inventory = $stateParams.inventory_id; + $stateParams[`${list.iterator}_search`].or__adhoccommand__inventory = $stateParams.inventory_id; + $stateParams[`${list.iterator}_search`].or__inventoryupdate__inventory_source__inventory = $stateParams.inventory_id; + + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + if (field.search) { + state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + } + + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js b/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js new file mode 100644 index 0000000000..53d91793ba --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js @@ -0,0 +1,88 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +export default ['i18n', function(i18n) { + return { + // These tooltip fields are consumed to build disabled related tabs tooltips in the form > add view + awToolTip: i18n._('Please save and run a job to view'), + dataPlacement: 'top', + name: 'completed_jobs', + basePath: 'unified_jobs', + iterator: 'completed_job', + search: { + "or__job__inventory": '' + }, + editTitle: i18n._('COMPLETED JOBS'), + index: false, + hover: true, + well: false, + emptyListText: i18n._('No completed jobs'), + + fields: { + status: { + label: '', + columnClass: 'List-staticColumn--smallStatus', + awToolTip: "{{ completed_job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ completed_job.status_popover_title }}", + icon: 'icon-job-{{ completed_job.status }}', + iconOnly: true, + ngClick:"viewjobResults(completed_job)", + }, + id: { + label: 'ID', + ngClick:"viewjobResults(completed_job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ completed_job.status_tip }}", + dataPlacement: 'top' + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6', + ngClick: "viewjobResults(completed_job)", + awToolTip: "{{ completed_job.name | sanitize }}", + dataPlacement: 'top' + }, + type: { + label: i18n._('Type'), + ngBind: 'completed_job.type_label', + link: false, + columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs", + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-3 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true + } + }, + + actions: { }, + + fieldActions: { + + columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', + + submit: { + icon: 'icon-rocket', + mode: 'all', + ngClick: 'relaunchJob($event, completed_job.id)', + awToolTip: i18n._('Relaunch using the same parameters'), + dataPlacement: 'top', + ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start" + }, + "delete": { + mode: 'all', + ngClick: 'deleteJob(completed_job.id)', + awToolTip: i18n._('Delete the job'), + dataPlacement: 'top', + ngShow: 'completed_job.summary_fields.user_capabilities.delete' + } + } + };}]; diff --git a/awx/ui/client/src/inventories/completed_jobs/main.js b/awx/ui/client/src/inventories/completed_jobs/main.js new file mode 100644 index 0000000000..b34f919a78 --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import list from './completed_jobs.list'; +import buildInventoryCompletedJobsState from './build-inventory-completed-jobs-state.factory'; + +export default + angular.module('inventoryCompletedJobs', []) + .factory('InventoryCompletedJobsList', list) + .factory('buildInventoryCompletedJobsState', buildInventoryCompletedJobsState); diff --git a/awx/ui/client/src/inventories/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index 5199e7bb34..c8655fa4e6 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -10,70 +10,88 @@ * @description This form is for adding/editing a Group on the inventory page */ -export default { - addTitle: 'CREATE GROUP', - editTitle: '{{ name }}', - showTitle: true, - name: 'group', - basePath: 'groups', - parent: 'inventories.edit.groups', - // the parent node this generated state definition tree expects to attach to - stateTree: 'inventories', - // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab - // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit - activeEditState: 'inventories.edit.groups.editGroup', - detailsClick: "$state.go('inventories.edit.groups.editGroup')", - well: false, - fields: { - name: { - label: 'Name', - type: 'text', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - required: true, - tab: 'properties' +export default ['i18n', 'nestedGroupListState', +function(i18n, nestedGroupListState){ + return { + addTitle: 'CREATE GROUP', + editTitle: '{{ name }}', + showTitle: true, + name: 'group', + basePath: 'groups', + parent: 'inventories.edit.groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.edit', + detailsClick: "$state.go('inventories.edit.groups.edit')", + well: false, + tabs: true, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "

Variables defined here apply to all child groups and hosts.

" + + "

Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + tab: 'properties' + } }, - description: { - label: 'Description', - type: 'text', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - tab: 'properties' - }, - variables: { - label: 'Variables', - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - dataTitle: 'Group Variables', - dataPlacement: 'right', - parseTypeName: 'parseType', - awPopOver: "

Variables defined here apply to all child groups and hosts.

" + - "

Enter variables using either JSON or YAML syntax. Use the " + - "radio button to toggle between the two.

" + - "JSON:
\n" + - "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', - dataContainer: 'body', - tab: 'properties' - } - }, - buttons: { - cancel: { - ngClick: 'formCancel()', - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } }, - close: { - ngClick: 'formCancel()', - ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - save: { - ngClick: 'formSave()', - ngDisabled: true, - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + related: { + nested_groups: { + name: 'nested_groups', + ngClick: "$state.go('inventories.edit.groups.edit.nested_groups')", + include: "NestedGroupListDefinition", + includeForm: "NestedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'nested_group', + listState: nestedGroupListState, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState + }, + } - } -}; + }; +}]; diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 20baec28b1..ed95f5a394 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -31,17 +31,9 @@ export default { name: { label: 'Groups', key: true, - ngClick: "groupSelect(group.id)", + ngClick: "editGroup(group.id)", columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', class: 'InventoryManage-breakWord', - }, - total_groups: { - nosort: true, - label: '', - type: 'badgeCount', - ngHide: 'group.total_groups == 0', - noLink: true, - awToolTip: "{{group.name | sanitize}} contains {{group.total_groups}} {{group.total_groups === 1 ? 'child' : 'children'}}" } }, diff --git a/awx/ui/client/src/inventories/groups/main.js b/awx/ui/client/src/inventories/groups/main.js index 9461571a71..b189cae4a2 100644 --- a/awx/ui/client/src/inventories/groups/main.js +++ b/awx/ui/client/src/inventories/groups/main.js @@ -7,6 +7,7 @@ import groupList from './list/main'; import groupAdd from './add/main'; import groupEdit from './edit/main'; +import nestedGroups from './nested-groups/main'; import groupFormDefinition from './groups.form'; import groupListDefinition from './groups.list'; import service from './groups.service'; @@ -20,9 +21,10 @@ export default angular.module('group', [ groupList.name, groupAdd.name, - groupEdit.name + groupEdit.name, + nestedGroups.name ]) - .value('GroupForm', groupFormDefinition) + .factory('GroupForm', groupFormDefinition) .value('GroupList', groupListDefinition) .factory('GetHostsStatusMsg', GetHostsStatusMsg) .factory('GetSourceTypeOptions', GetSourceTypeOptions) diff --git a/awx/ui/client/src/inventories/groups/nested-groups/main.js b/awx/ui/client/src/inventories/groups/nested-groups/main.js new file mode 100644 index 0000000000..4d322b4270 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/main.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import nestedGroupListState from './nested-groups-list-state.factory'; +import nestedGroupListDefinition from './nested-groups.list'; +import nestedGroupFormDefinition from './nested-groups.form'; +import controller from './nested-groups-list.controller'; + +export default + angular.module('nestedGroups', []) + .factory('nestedGroupListState', nestedGroupListState) + .value('NestedGroupListDefinition', nestedGroupListDefinition) + .factory('NestedGroupFormDefinition', nestedGroupFormDefinition) + .controller('NestedGroupsListController', controller); diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js new file mode 100644 index 0000000000..67007647b5 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -0,0 +1,224 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'NestedGroupListDefinition', 'InventoryUpdate', + 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, NestedGroupListDefinition, InventoryUpdate, + GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, + GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + + let list = NestedGroupListDefinition; + + init(); + + function init(){ + $scope.inventory_id = $stateParams.inventory_id; + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. + // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: + // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the + // problem that this solves. + $scope.ncyBreadcrumbIgnore = true; + if($state.current.name === "inventoryManage.editGroup") { + $scope.rowBeingEdited = $state.params.group_id; + $scope.listBeingEdited = "groups"; + } + + $scope.inventory_id = $stateParams.inventory_id; + _.forEach($scope[list.name], buildStatusIndicators); + + } + + function buildStatusIndicators(group){ + if (group === undefined || group === null) { + group = {}; + } + + let hosts_status; + + hosts_status = GetHostsStatusMsg({ + active_failures: group.hosts_with_active_failures, + total_hosts: group.total_hosts, + inventory_id: $scope.inventory_id, + group_id: group.id + }); + _.assign(group, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}); + } + + $scope.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $scope.createGroup = function(){ + $state.go('inventories.edit.groups.add'); + }; + $scope.editGroup = function(id){ + $state.go('inventories.edit.groups.edit', {group_id: id}); + }; + $scope.deleteGroup = function(group){ + $scope.toDelete = {}; + angular.extend($scope.toDelete, group); + if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) { + // This group doesn't have any child groups or hosts - the user is just trying to delete + // the group + $scope.deleteOption = "delete"; + } + $('#group-delete-modal').modal('show'); + }; + $scope.confirmDelete = function(){ + + // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes + // will mean that these two things are running async and the modal may not finish closing before + // the state finishes transitioning. + $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () { + // Remove the event handler so that we don't end up with multiple bindings + $('#group-delete-modal').off('hidden.bs.modal'); + // Reload the inventory manage page and show that the group has been removed + $state.go('inventoryManage', null, {reload: true}); + }); + + switch($scope.deleteOption){ + case 'promote': + GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id) + .then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("inventoryManage", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + break; + default: + GroupManageService.delete($scope.toDelete.id).then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("inventoryManage", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + } + }; + $scope.updateGroup = function(group) { + GroupManageService.getInventorySource({group: group.id}).then(res =>InventoryUpdate({ + scope: $scope, + group_id: group.id, + url: res.data.results[0].related.update, + group_name: group.name, + group_source: res.data.results[0].source + })); + }; + + $scope.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + qs.search(path, $state.params[`${list.iterator}_search`]) + .then(function(searchResponse) { + $scope[`${list.iterator}_dataset`] = searchResponse.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $scope.rowBeingEdited = toParams.group_id; + $scope.listBeingEdited = "groups"; + } + else { + delete $scope.rowBeingEdited; + delete $scope.listBeingEdited; + } + }); + + // Remove the listener when the scope is destroyed to avoid a memory leak + $scope.$on('$destroy', function() { + cleanUpStateChangeListener(); + }); + + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js new file mode 100644 index 0000000000..9d4645018d --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js @@ -0,0 +1,98 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Groups + * @description This form is for adding/editing a Group on the inventory page +*/ + +export default ['i18n', 'nestedGroupListState', +function(i18n, nestedGroupListState){ + return { + addTitle: 'CREATE GROUP', + editTitle: '{{ name }}', + showTitle: true, + name: 'nested_group', + iterator: "nested_group", + basePath: 'groups', + parent: 'inventories.edit.groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.edit', + detailsClick: "$state.go('inventories.edit.groups.edit')", + well: false, + tabs: true, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "

Variables defined here apply to all child groups and hosts.

" + + "

Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.

" + + "JSON:
\n" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + tab: 'properties' + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + }, + related: { + nested_groups: { + name: 'related_groups', + ngClick: "$state.go('inventories.edit.groups.edit.related_groups')", + include: "RelatedGroupListDefinition", + includeForm: "RelatedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'related_group', + listState: nestedGroupListState, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState + }, + + } + }; +}]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js new file mode 100644 index 0000000000..1e28c83454 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -0,0 +1,146 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'nested_groups', + iterator: 'nested_group', + editTitle: '{{ inventory.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + multiSelect: true, + trackBy: 'nested_group.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', + + fields: { + failed_hosts: { + label: '', + nosort: true, + mode: 'all', + iconOnly: true, + awToolTip: "{{ group.hosts_status_tip }}", + dataPlacement: "top", + ngClick: "showFailedHosts(group)", + icon: "{{ 'fa icon-job-' + group.hosts_status_class }}", + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + label: 'Groups', + key: true, + + // ngClick: "groupSelect(group.id)", + ngClick: "editGroup(group.id)", + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', + class: 'InventoryManage-breakWord', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + launch: { + mode: 'all', + // $scope.$parent is governed by InventoryManageController, + ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', + ngClick: '$parent.setAdhocPattern()', + awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", + dataTipWatch: "adhocCommandTooltip", + actionClass: 'btn List-buttonDefault', + buttonContent: 'RUN COMMANDS', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + ngShow: 'canAdhoc' + // TODO: set up a tip watcher and change text based on when + // things are selected/not selected. This is started and + // commented out in the inventory controller within the watchers. + // awToolTip: "{{ adhocButtonTipContents }}", + // dataTipWatch: "adhocButtonTipContents" + }, + create: { + mode: 'all', + ngClick: "createGroup()", + awToolTip: "Create a new group", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD GROUP', + ngShow: 'canAdd', + dataPlacement: "top", + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', + + // group_update: { + // //label: 'Sync', + // mode: 'all', + // ngClick: 'updateGroup(group)', + // awToolTip: "{{ group.launch_tooltip }}", + // dataTipWatch: "group.launch_tooltip", + // ngShow: "(group.status !== 'running' && group.status " + + // "!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start", + // ngClass: "group.launch_class", + // dataPlacement: "top", + // }, + // cancel: { + // //label: 'Cancel', + // mode: 'all', + // ngClick: "cancelUpdate(group.id)", + // awToolTip: "Cancel sync process", + // 'class': 'red-txt', + // ngShow: "(group.status == 'running' || group.status == 'pending' " + + // "|| group.status == 'updating') && group.summary_fields.user_capabilities.start", + // dataPlacement: "top", + // iconClass: "fa fa-minus-circle" + // }, + copy: { + mode: 'all', + ngClick: "copyMoveGroup(group.id)", + awToolTip: 'Copy or move group', + ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy", + dataPlacement: "top" + }, + // schedule: { + // mode: 'all', + // ngClick: "scheduleGroup(group.id)", + // awToolTip: "{{ group.group_schedule_tooltip }}", + // ngClass: "group.scm_type_class", + // dataPlacement: 'top', + // ngShow: "!(group.summary_fields.inventory_source.source === '')" + // }, + edit: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(group.id)", + awToolTip: 'Edit group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(group.id)", + awToolTip: 'View group', + dataPlacement: "top", + ngShow: "!group.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteGroup(group)", + awToolTip: 'Delete group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index b59b2ebe39..c79be281f4 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -13,10 +13,29 @@ export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', 'buildHostListState', 'buildHostAddState', 'buildHostEditState', 'buildSourcesListState', 'buildSourcesAddState', - 'buildSourcesEditState', + 'buildSourcesEditState', 'buildInventoryCompletedJobsState', + 'InventoryCompletedJobsList', function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, buildHostListState, buildHostAddState, buildHostEditState, - buildSourcesListState, buildSourcesAddState,buildSourcesEditState) { + buildSourcesListState, buildSourcesAddState,buildSourcesEditState, + buildInventoryCompletedJobsState, InventoryCompletedJobsList) { + + var completed_jobs_object = { + name: 'completed_jobs', + index: false, + basePath: "unified_jobs", + include: "InventoryCompletedJobsList", + title: i18n._('Completed Jobs'), + iterator: 'completed_job', + generateList: true, + listState: buildInventoryCompletedJobsState, + search: { + "or__job__inventory": '' + } + }; + let clone = _.clone(InventoryCompletedJobsList); + completed_jobs_object = angular.extend(clone, completed_jobs_object); + return { addTitle: i18n._('NEW INVENTORY'), @@ -96,7 +115,7 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, name: 'permissions', awToolTip: i18n._('Please save before assigning permissions'), dataPlacement: 'top', - basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/access_list/', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/access_list/', type: 'collection', title: i18n._('Permissions'), iterator: 'permission', @@ -165,39 +184,7 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, addState: buildSourcesAddState, editState: buildSourcesEditState }, - //this is a placeholder for when we're ready for completed jobs - completed_jobs: { - name: 'completed_jobs', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/completed_jobs/', - type: 'collection', - title: i18n._('Completed Jobs'), - iterator: 'completed_job', - index: false, - open: false, - // search: { - // order_by: 'username' - // }, - actions: { - add: { - label: i18n._('Add'), - ngClick: "$state.go('.add')", - awToolTip: i18n._('Add a permission'), - actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD', - // ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - - } - }, - fields: { - name: { - label: i18n._('Name'), - // linkBase: 'users', - class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' - } - } - } + completed_jobs: completed_jobs_object } };}]; diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 74a3af0e1b..0d9c4c3f84 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -8,6 +8,7 @@ import host from './hosts/main'; import group from './groups/main'; import sources from './sources/main'; import relatedHost from './related-hosts/main'; +import inventoryCompletedJobs from './completed_jobs/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; @@ -22,6 +23,7 @@ angular.module('inventory', [ group.name, sources.name, relatedHost.name, + inventoryCompletedJobs.name, inventoryAdd.name, inventoryEdit.name, inventoryList.name diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 785a1414e7..9b9ce3dbf5 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -576,6 +576,7 @@ function($injector, $stateExtender, $log, i18n) { if(field.includeForm){ let form = field.includeForm ? $injector.get(field.includeForm) : field; states.push(that.generateLookupNodes(form, formState)); + states.push(that.generateFormListDefinitions(form, formState, params)); } states = _.flatten(states); } @@ -678,6 +679,7 @@ function($injector, $stateExtender, $log, i18n) { if (field.search) { state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); } + return state; } return _(form.related).map(buildListNodes).flatten().value();