From a49095bdbca6481beef9c60b25a6e3bc87ae347d Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 28 Oct 2016 14:28:06 -0400 Subject: [PATCH] Boolean / Smart Search (#3631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Part 1: building new search components Directives: smart-search, column-sort, paginate Service: QuerySet Model: DjangoSearchModel * Part 2: Implementing new search components, de-implementing old search components Remove old code: * tagSearch directive * old pagination strategy * old column sorting strategy * lookup Add new directives to list/form generator: * smart-search, * paginate * column-sort Connect $state + dataset resolution * upgrade ui-router lib to v1.0.0-beta3 * Custom $urlMatcherFactory.type - queryset * Render lists, forms, related, lookups in named views * Provide html templates in list/form/lookup/related state definitions * Provide dataset through resolve block in state definitions Update utilities * isEmpty filter * use async validation strategy in awlookup directive * Part 3: State implementations (might split into per-module commits) * Support optional state definition flag: squashSearchUrl. *_search params are only URI-encoded if squashSearchUrl is falsey. * * Fix list badge counts * Clear search input after search term(s) applied * Chain of multiple search terms in one submission * Hook up activity stream * Hook up portal mode * Fix pagination range calculations * Hook up organization sub-list views * Hook up listDefinition.search defaults * Fix ng-disabled conditions reflecting RBAC access on form fields * Fix actively-editing indicator in generated lists * form generator - fix undefined span, remove dead event listeners * wrap hosts/groups lists in a panel, fix groups list error * Smart search directive: clear all search tags * Search tags - ‘Clear All’ text - 12px Search key - remove top padding/margin Search key - reverse bolding of relationship fields / label, add commas Search tags - remove padding-bottom Lookup modal - “X” close button styled incorrectly Lookup modal - List title not rendered Lookup modal - 20px margin between buttons * Portal Mode Fix default column-sort on jobs list Hide column-oort on job status column Apply custom search bar sizes * stateDefinition.factory Return ES6 Promise instead of $q promise. $q cannot be safely provided during module.config() phase Some generated state trees (inventory / inventoryManage) need to be reduced to one promise. Side-step issues caused by ui-router de-registering ALL registered states that match placeholder state name/url pattern. e.g. inventories.lazyLoad() would de-register inventoryManage states if a page refresh occured @ /#/inventories/** * Combine generated state trees: inventories + inventoryManage Hook up inventory sync schedule list/form add /form edit views * Hook up system job schedule list/add/edit states * Fix breadcrumb of generated states in /setup view Fix typo in scheduler search prefix * Remove old search system deritus from list definitions * Fix breadcrumb definitions in states registered in app.js config block * Transclude list action buttons in generated form lists * Lookup Modal passes acceptance criterea: Modal cancel/exit - don’t update form field’s ng-model Modal save - do update form field's ng-model Transclude generated list contents into directive Lookup modal test spec * Fix typo in merge conflict resolution * Disable failing unit tests pending revision * Integrate smart-search architechture into add-permissions modal * use a semicolon delimiter instead of comma to avoid collision with django __in comparator * Hook up Dashboard > Hosts states, update Dashboard Inventory/Project counts with new search filters * Misc bug splat Add 20px spacing around root ui-view Fix missing closing div in related views Remove dupe line in smart-search controller * Remove defunct LookupHelper code * Rebuild inventories list status tooltips on updates to dataset Code cleanup - remove defunct modules Remove LookupHelper / LookupInit code Remove pre-RBAC permissions module * Add mising stateTree / basePath properties to form definitions * Resolve i18n conflicts in list and form generator Freeze dependencies * Integrate sockets * Final bug splat: fix jobs > job details and jobs > scheduled routing fix mis-resolved merge conflicts swap console.info for $log.debug --- awx/ui/client/legacy-styles/ansible-ui.less | 61 +- awx/ui/client/legacy-styles/jobs.less | 6 - awx/ui/client/legacy-styles/lists.less | 38 - .../addPermissions.controller.js | 133 +- .../addPermissions.directive.js | 73 +- .../addPermissions.partial.html | 10 +- .../addPermissionsList.directive.js | 80 +- .../permissionsUsers.list.js | 8 +- .../activitystream.controller.js | 35 +- .../activitystream.partial.html | 3 - .../activity-stream/activitystream.route.js | 89 +- .../stream-dropdown-nav.directive.js | 11 +- awx/ui/client/src/app.js | 501 ++--- .../src/bread-crumb/bread-crumb.directive.js | 18 +- awx/ui/client/src/controllers/Credentials.js | 533 ++--- awx/ui/client/src/controllers/JobEvents.js | 82 +- awx/ui/client/src/controllers/JobHosts.js | 53 +- awx/ui/client/src/controllers/Jobs.js | 209 +- awx/ui/client/src/controllers/Projects.js | 551 ++--- awx/ui/client/src/controllers/Schedules.js | 53 +- awx/ui/client/src/controllers/Teams.js | 324 +-- awx/ui/client/src/controllers/Users.js | 448 ++-- .../counts/dashboard-counts.directive.js | 6 +- .../hosts/dashboard-hosts-edit.partial.html | 4 - .../hosts/dashboard-hosts-list.controller.js | 130 +- .../hosts/dashboard-hosts-list.partial.html | 4 - .../dashboard/hosts/dashboard-hosts.form.js | 6 +- .../dashboard/hosts/dashboard-hosts.list.js | 19 - .../dashboard/hosts/dashboard-hosts.route.js | 61 - awx/ui/client/src/dashboard/hosts/main.js | 38 +- awx/ui/client/src/forms/Credentials.js | 102 +- awx/ui/client/src/forms/Groups.js | 89 +- awx/ui/client/src/forms/HostGroups.js | 3 +- awx/ui/client/src/forms/Hosts.js | 26 +- awx/ui/client/src/forms/Inventories.js | 213 +- awx/ui/client/src/forms/JobTemplates.js | 129 +- awx/ui/client/src/forms/JobVarsPrompt.js | 6 +- awx/ui/client/src/forms/Jobs.js | 1 + awx/ui/client/src/forms/Organizations.js | 27 +- awx/ui/client/src/forms/Projects.js | 92 +- awx/ui/client/src/forms/Teams.js | 53 +- awx/ui/client/src/forms/Users.js | 87 +- awx/ui/client/src/helpers.js | 10 - awx/ui/client/src/helpers/Adhoc.js | 2 +- awx/ui/client/src/helpers/Credentials.js | 17 +- awx/ui/client/src/helpers/Groups.js | 39 +- awx/ui/client/src/helpers/Hosts.js | 111 +- awx/ui/client/src/helpers/JobDetail.js | 154 +- awx/ui/client/src/helpers/JobSubmission.js | 2 +- awx/ui/client/src/helpers/JobTemplates.js | 71 +- awx/ui/client/src/helpers/Jobs.js | 170 +- .../client/src/helpers/PaginationHelpers.js | 182 -- awx/ui/client/src/helpers/Schedules.js | 232 +-- awx/ui/client/src/helpers/inventory.js | 6 +- awx/ui/client/src/helpers/refresh-related.js | 59 - awx/ui/client/src/helpers/refresh.js | 104 - awx/ui/client/src/helpers/related-search.js | 295 --- awx/ui/client/src/helpers/search.js | 536 ----- awx/ui/client/src/helpers/teams.js | 42 +- .../add/inventory-add.controller.js | 73 +- .../inventories/add/inventory-add.route.js | 19 - awx/ui/client/src/inventories/add/main.js | 8 +- .../edit/inventory-edit.controller.js | 333 +-- .../inventories/edit/inventory-edit.route.js | 22 - awx/ui/client/src/inventories/edit/main.js | 6 +- .../src/inventories/inventories.partial.html | 5 - .../list/inventory-list.controller.js | 187 +- .../inventories/list/inventory-list.route.js | 22 - awx/ui/client/src/inventories/list/main.js | 8 +- awx/ui/client/src/inventories/main.js | 219 +- .../manage/adhoc/adhoc.controller.js | 15 +- .../inventories/manage/adhoc/adhoc.form.js | 13 +- .../inventories/manage/adhoc/adhoc.route.js | 7 +- .../src/inventories/manage/adhoc/main.js | 6 +- .../copy-move/copy-move-groups.controller.js | 32 +- .../copy-move/copy-move-hosts.controller.js | 32 +- .../manage/copy-move/copy-move.route.js | 20 +- .../src/inventories/manage/copy-move/main.js | 9 +- .../manage/groups/groups-add.controller.js | 147 +- .../manage/groups/groups-edit.controller.js | 250 +-- .../manage/groups/groups-form.partial.html | 4 - .../manage/groups/groups-list.controller.js | 120 +- .../manage/groups/groups-list.partial.html | 1 - .../inventories/manage/groups/groups.route.js | 61 - .../src/inventories/manage/groups/main.js | 11 +- .../manage/hosts/hosts-add.controller.js | 65 +- .../manage/hosts/hosts-edit.controller.js | 42 +- .../manage/hosts/hosts-form.partial.html | 4 - .../manage/hosts/hosts-list.controller.js | 69 +- .../inventories/manage/hosts/hosts.route.js | 60 - .../src/inventories/manage/hosts/main.js | 57 +- .../manage/inventory-manage.partial.html | 11 +- .../manage/inventory-manage.route.js | 104 +- .../manage/inventory-manage.service.js | 16 +- awx/ui/client/src/inventories/manage/main.js | 8 +- .../inventory-scripts/add/add.controller.js | 94 +- .../inventory-scripts/add/add.partial.html | 3 - .../src/inventory-scripts/add/add.route.js | 18 - .../client/src/inventory-scripts/add/main.js | 6 +- .../inventory-scripts/edit/edit.controller.js | 138 +- .../inventory-scripts/edit/edit.partial.html | 3 - .../src/inventory-scripts/edit/edit.route.js | 49 - .../client/src/inventory-scripts/edit/main.js | 6 +- .../inventory-scripts.form.js | 35 +- .../inventory-scripts/list/list.controller.js | 148 +- .../inventory-scripts/list/list.partial.html | 4 - .../src/inventory-scripts/list/list.route.js | 22 - .../client/src/inventory-scripts/list/main.js | 10 +- awx/ui/client/src/inventory-scripts/main.js | 64 +- .../job-detail/host-event/host-event.route.js | 129 +- .../host-events/host-events.block.less | 11 +- .../host-events/host-events.controller.js | 90 +- .../host-events/host-events.partial.html | 7 +- .../host-events/host-events.route.js | 6 - .../host-summary/host-summary.controller.js | 43 +- .../host-summary/host-summary.route.js | 6 - .../src/job-detail/job-detail.controller.js | 248 ++- .../client/src/job-detail/job-detail.route.js | 14 +- .../job-submission.controller.js | 72 +- .../add/inventory-job-templates-add.route.js | 17 - .../add/job-templates-add.controller.js | 151 +- .../add/job-templates-add.partial.html | 5 - .../add/job-templates-add.route.js | 32 - awx/ui/client/src/job-templates/add/main.js | 8 +- .../inventory-job-templates-edit.route.js | 17 - .../edit/job-templates-edit.controller.js | 155 +- .../edit/job-templates-edit.partial.html | 5 - .../edit/job-templates-edit.route.js | 34 - awx/ui/client/src/job-templates/edit/main.js | 8 +- .../labels/labelsList.directive.js | 3 +- .../list/job-templates-list.controller.js | 169 +- .../list/job-templates-list.partial.html | 6 - .../list/job-templates-list.route.js | 26 - awx/ui/client/src/job-templates/list/main.js | 6 +- awx/ui/client/src/job-templates/main.js | 38 +- .../shared/question-definition.form.js | 39 +- awx/ui/client/src/lists/AllJobs.js | 48 +- awx/ui/client/src/lists/CompletedJobs.js | 27 +- awx/ui/client/src/lists/Credentials.js | 3 - awx/ui/client/src/lists/Inventories.js | 28 +- awx/ui/client/src/lists/InventoryGroups.js | 63 +- awx/ui/client/src/lists/InventoryHosts.js | 20 - awx/ui/client/src/lists/JobEvents.js | 14 - awx/ui/client/src/lists/JobHosts.js | 29 +- awx/ui/client/src/lists/JobTemplates.js | 6 +- awx/ui/client/src/lists/Jobs.js | 20 +- awx/ui/client/src/lists/PortalJobTemplates.js | 1 - awx/ui/client/src/lists/PortalJobs.js | 9 +- awx/ui/client/src/lists/Projects.js | 11 +- awx/ui/client/src/lists/ScheduledJobs.js | 17 +- awx/ui/client/src/lists/Schedules.js | 6 +- awx/ui/client/src/lists/Streams.js | 17 +- awx/ui/client/src/lists/Users.js | 11 +- .../src/login/loginBackDrop.partial.html | 2 +- awx/ui/client/src/lookup/lookup.block.less | 26 - awx/ui/client/src/lookup/lookup.factory.js | 307 --- awx/ui/client/src/lookup/main.js | 12 - .../management-jobs/card/card.controller.js | 23 +- .../notifications/notification.controller.js | 31 +- .../src/management-jobs/scheduler/main.js | 101 +- .../src/notifications/add/add.controller.js | 307 ++- .../src/notifications/add/add.partial.html | 3 - .../client/src/notifications/add/add.route.js | 18 - awx/ui/client/src/notifications/add/main.js | 6 +- .../src/notifications/edit/edit.controller.js | 456 ++-- .../src/notifications/edit/edit.partial.html | 3 - .../src/notifications/edit/edit.route.js | 48 - awx/ui/client/src/notifications/edit/main.js | 6 +- awx/ui/client/src/notifications/main.js | 84 +- .../list.controller.js | 358 ++-- .../list.partial.html | 4 - .../notification-templates-list/list.route.js | 18 - .../notification-templates-list/main.js | 6 +- .../notificationTemplates.form.js | 86 +- .../shared/toggle-notification.factory.js | 3 +- awx/ui/client/src/organizations/add/main.js | 13 - .../add/organizations-add.controller.js | 35 +- .../add/organizations-add.partial.html | 4 - .../add/organizations-add.route.js | 19 - awx/ui/client/src/organizations/edit/main.js | 15 - .../edit/organizations-edit.controller.js | 133 +- .../edit/organizations-edit.route.js | 24 - .../linkout/addUsers/addUsers.controller.js | 47 +- .../linkout/addUsers/addUsers.directive.js | 7 +- .../linkout/addUsers/addUsers.partial.html | 6 +- .../organizations-admins.controller.js | 127 +- .../organizations-inventories.controller.js | 487 ++--- .../organizations-job-templates.controller.js | 94 +- .../organizations-projects.controller.js | 577 +++--- .../organizations-teams.controller.js | 116 +- .../organizations-users.controller.js | 142 +- .../client/src/organizations/linkout/main.js | 7 +- .../organizations-linkout.partial.html | 4 - .../linkout/organizations-linkout.route.js | 530 +++-- awx/ui/client/src/organizations/list/main.js | 13 - .../list/organizations-list.controller.js | 143 +- .../list/organizations-list.partial.html | 47 +- .../list/organizations-list.route.js | 26 - awx/ui/client/src/organizations/main.js | 64 +- awx/ui/client/src/partials/jobs.html | 34 +- awx/ui/client/src/partials/projects.html | 5 - .../src/permissions/add/add.controller.js | 157 -- awx/ui/client/src/permissions/add/main.js | 17 - .../src/permissions/add/team-add.route.js | 14 - .../src/permissions/add/user-add.route.js | 14 - .../src/permissions/edit/edit.controller.js | 184 -- awx/ui/client/src/permissions/edit/main.js | 17 - .../src/permissions/edit/team-edit.route.js | 14 - .../src/permissions/edit/user-edit.route.js | 14 - .../src/permissions/list/list.controller.js | 143 -- awx/ui/client/src/permissions/list/main.js | 17 - .../src/permissions/list/team-list.route.js | 14 - .../src/permissions/list/user-list.route.js | 14 - awx/ui/client/src/permissions/main.js | 26 - .../shared/category-change.factory.js | 74 - .../shared/get-search-select.factory.js | 29 - .../permissions/shared/permissions.form.js | 147 -- .../permissions/shared/permissions.list.js | 75 - .../shared/team-permissions.partial.html | 3 - .../shared/user-permissions.partial.html | 3 - .../portal-mode-job-templates.controller.js | 50 +- .../portal-mode-job-templates.partial.html | 4 - .../portal-mode-jobs.controller.js | 83 +- .../portal-mode-layout.partial.html | 28 +- .../src/portal-mode/portal-mode.route.js | 94 +- .../client/src/rest/restServices.factory.js | 13 +- awx/ui/client/src/scheduler/main.js | 138 +- .../src/scheduler/scheduler.controller.js | 138 -- .../src/scheduler/schedulerAdd.controller.js | 9 +- .../src/scheduler/schedulerEdit.controller.js | 6 +- .../src/scheduler/schedulerList.controller.js | 123 ++ .../src/search/getSearchHtml.service.js | 51 - awx/ui/client/src/search/main.js | 9 - awx/ui/client/src/search/tagSearch.block.less | 254 --- .../client/src/search/tagSearch.controller.js | 132 -- .../client/src/search/tagSearch.directive.js | 32 - .../client/src/search/tagSearch.partial.html | 79 - awx/ui/client/src/search/tagSearch.service.js | 226 -- awx/ui/client/src/shared/Utilities.js | 5 +- .../column-sort/column-sort.controller.js | 54 + .../column-sort/column-sort.directive.js | 19 + .../column-sort/column-sort.partial.html | 4 + awx/ui/client/src/shared/column-sort/main.js | 7 + awx/ui/client/src/shared/directives.js | 1839 ++++++++--------- awx/ui/client/src/shared/filters.js | 13 + awx/ui/client/src/shared/form-generator.js | 712 ++----- awx/ui/client/src/shared/generator-helpers.js | 166 +- .../list-generator/list-generator.factory.js | 925 ++++----- .../src/shared/lookup/lookup-modal.block.less | 6 + .../shared/lookup/lookup-modal.directive.js | 34 + .../shared/lookup/lookup-modal.partial.html | 24 + awx/ui/client/src/shared/lookup/main.js | 11 + awx/ui/client/src/shared/main.js | 24 +- .../select-list-item.directive.js | 2 +- awx/ui/client/src/shared/paginate/main.js | 13 + .../src/shared/paginate/paginate.block.less | 53 + .../shared/paginate/paginate.controller.js | 66 + .../src/shared/paginate/paginate.directive.js | 16 + .../src/shared/paginate/paginate.partial.html | 44 + awx/ui/client/src/shared/pagination/main.js | 11 - .../shared/pagination/pagination.service.js | 44 - .../smart-search/django-search-model.class.js | 56 + awx/ui/client/src/shared/smart-search/main.js | 12 + .../shared/smart-search/queryset.service.js | 116 ++ .../smart-search/smart-search.block.less | 234 +++ .../smart-search/smart-search.controller.js | 113 + .../smart-search/smart-search.directive.js | 23 + .../smart-search/smart-search.partial.html | 57 + .../src/shared/socket/socket.service.js | 21 +- .../src/shared/stateDefinitions.factory.js | 434 ++++ .../src/shared/stateExtender.provider.js | 51 +- .../adhoc/standard-out-adhoc.route.js | 16 +- .../standard-out-management-jobs.route.js | 14 +- .../standard-out-scm-update.route.js | 14 +- awx/ui/client/src/widgets/Stream.js | 62 +- awx/ui/karma.conf.js | 2 + awx/ui/npm-shrinkwrap.json | 16 +- awx/ui/package.json | 2 +- awx/ui/templates/ui/index.html | 310 ++- .../lookup/lookup-modal.directive-test.js | 131 ++ .../spec/paginate/paginate.directive-test.js | 116 ++ .../smart-search/queryset.service-test.js | 85 + .../smart-search.directive-test.js | 163 ++ 283 files changed, 9625 insertions(+), 14375 deletions(-) delete mode 100644 awx/ui/client/src/activity-stream/activitystream.partial.html delete mode 100644 awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.partial.html delete mode 100644 awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.partial.html delete mode 100644 awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js delete mode 100644 awx/ui/client/src/helpers/PaginationHelpers.js delete mode 100644 awx/ui/client/src/helpers/refresh-related.js delete mode 100644 awx/ui/client/src/helpers/refresh.js delete mode 100644 awx/ui/client/src/helpers/related-search.js delete mode 100644 awx/ui/client/src/helpers/search.js delete mode 100644 awx/ui/client/src/inventories/add/inventory-add.route.js delete mode 100644 awx/ui/client/src/inventories/edit/inventory-edit.route.js delete mode 100644 awx/ui/client/src/inventories/inventories.partial.html delete mode 100644 awx/ui/client/src/inventories/list/inventory-list.route.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups-form.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups.route.js delete mode 100644 awx/ui/client/src/inventories/manage/hosts/hosts-form.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/hosts/hosts.route.js delete mode 100644 awx/ui/client/src/inventory-scripts/add/add.partial.html delete mode 100644 awx/ui/client/src/inventory-scripts/add/add.route.js delete mode 100644 awx/ui/client/src/inventory-scripts/edit/edit.partial.html delete mode 100644 awx/ui/client/src/inventory-scripts/edit/edit.route.js delete mode 100644 awx/ui/client/src/inventory-scripts/list/list.partial.html delete mode 100644 awx/ui/client/src/inventory-scripts/list/list.route.js delete mode 100644 awx/ui/client/src/job-templates/add/inventory-job-templates-add.route.js delete mode 100644 awx/ui/client/src/job-templates/add/job-templates-add.partial.html delete mode 100644 awx/ui/client/src/job-templates/add/job-templates-add.route.js delete mode 100644 awx/ui/client/src/job-templates/edit/inventory-job-templates-edit.route.js delete mode 100644 awx/ui/client/src/job-templates/edit/job-templates-edit.partial.html delete mode 100644 awx/ui/client/src/job-templates/edit/job-templates-edit.route.js delete mode 100644 awx/ui/client/src/job-templates/list/job-templates-list.partial.html delete mode 100644 awx/ui/client/src/job-templates/list/job-templates-list.route.js delete mode 100644 awx/ui/client/src/lookup/lookup.block.less delete mode 100644 awx/ui/client/src/lookup/lookup.factory.js delete mode 100644 awx/ui/client/src/lookup/main.js delete mode 100644 awx/ui/client/src/notifications/add/add.partial.html delete mode 100644 awx/ui/client/src/notifications/add/add.route.js delete mode 100644 awx/ui/client/src/notifications/edit/edit.partial.html delete mode 100644 awx/ui/client/src/notifications/edit/edit.route.js delete mode 100644 awx/ui/client/src/notifications/notification-templates-list/list.partial.html delete mode 100644 awx/ui/client/src/notifications/notification-templates-list/list.route.js delete mode 100644 awx/ui/client/src/organizations/add/main.js delete mode 100644 awx/ui/client/src/organizations/add/organizations-add.partial.html delete mode 100644 awx/ui/client/src/organizations/add/organizations-add.route.js delete mode 100644 awx/ui/client/src/organizations/edit/main.js delete mode 100644 awx/ui/client/src/organizations/edit/organizations-edit.route.js delete mode 100644 awx/ui/client/src/organizations/linkout/organizations-linkout.partial.html delete mode 100644 awx/ui/client/src/organizations/list/main.js delete mode 100644 awx/ui/client/src/organizations/list/organizations-list.route.js delete mode 100644 awx/ui/client/src/partials/projects.html delete mode 100644 awx/ui/client/src/permissions/add/add.controller.js delete mode 100644 awx/ui/client/src/permissions/add/main.js delete mode 100644 awx/ui/client/src/permissions/add/team-add.route.js delete mode 100644 awx/ui/client/src/permissions/add/user-add.route.js delete mode 100644 awx/ui/client/src/permissions/edit/edit.controller.js delete mode 100644 awx/ui/client/src/permissions/edit/main.js delete mode 100644 awx/ui/client/src/permissions/edit/team-edit.route.js delete mode 100644 awx/ui/client/src/permissions/edit/user-edit.route.js delete mode 100644 awx/ui/client/src/permissions/list/list.controller.js delete mode 100644 awx/ui/client/src/permissions/list/main.js delete mode 100644 awx/ui/client/src/permissions/list/team-list.route.js delete mode 100644 awx/ui/client/src/permissions/list/user-list.route.js delete mode 100644 awx/ui/client/src/permissions/main.js delete mode 100644 awx/ui/client/src/permissions/shared/category-change.factory.js delete mode 100644 awx/ui/client/src/permissions/shared/get-search-select.factory.js delete mode 100644 awx/ui/client/src/permissions/shared/permissions.form.js delete mode 100644 awx/ui/client/src/permissions/shared/permissions.list.js delete mode 100644 awx/ui/client/src/permissions/shared/team-permissions.partial.html delete mode 100644 awx/ui/client/src/permissions/shared/user-permissions.partial.html delete mode 100644 awx/ui/client/src/portal-mode/portal-mode-job-templates.partial.html delete mode 100644 awx/ui/client/src/scheduler/scheduler.controller.js create mode 100644 awx/ui/client/src/scheduler/schedulerList.controller.js delete mode 100644 awx/ui/client/src/search/getSearchHtml.service.js delete mode 100644 awx/ui/client/src/search/main.js delete mode 100644 awx/ui/client/src/search/tagSearch.block.less delete mode 100644 awx/ui/client/src/search/tagSearch.controller.js delete mode 100644 awx/ui/client/src/search/tagSearch.directive.js delete mode 100644 awx/ui/client/src/search/tagSearch.partial.html delete mode 100644 awx/ui/client/src/search/tagSearch.service.js create mode 100644 awx/ui/client/src/shared/column-sort/column-sort.controller.js create mode 100644 awx/ui/client/src/shared/column-sort/column-sort.directive.js create mode 100644 awx/ui/client/src/shared/column-sort/column-sort.partial.html create mode 100644 awx/ui/client/src/shared/column-sort/main.js create mode 100644 awx/ui/client/src/shared/lookup/lookup-modal.block.less create mode 100644 awx/ui/client/src/shared/lookup/lookup-modal.directive.js create mode 100644 awx/ui/client/src/shared/lookup/lookup-modal.partial.html create mode 100644 awx/ui/client/src/shared/lookup/main.js create mode 100644 awx/ui/client/src/shared/paginate/main.js create mode 100644 awx/ui/client/src/shared/paginate/paginate.block.less create mode 100644 awx/ui/client/src/shared/paginate/paginate.controller.js create mode 100644 awx/ui/client/src/shared/paginate/paginate.directive.js create mode 100644 awx/ui/client/src/shared/paginate/paginate.partial.html delete mode 100644 awx/ui/client/src/shared/pagination/main.js delete mode 100644 awx/ui/client/src/shared/pagination/pagination.service.js create mode 100644 awx/ui/client/src/shared/smart-search/django-search-model.class.js create mode 100644 awx/ui/client/src/shared/smart-search/main.js create mode 100644 awx/ui/client/src/shared/smart-search/queryset.service.js create mode 100644 awx/ui/client/src/shared/smart-search/smart-search.block.less create mode 100644 awx/ui/client/src/shared/smart-search/smart-search.controller.js create mode 100644 awx/ui/client/src/shared/smart-search/smart-search.directive.js create mode 100644 awx/ui/client/src/shared/smart-search/smart-search.partial.html create mode 100644 awx/ui/client/src/shared/stateDefinitions.factory.js create mode 100644 awx/ui/tests/spec/lookup/lookup-modal.directive-test.js create mode 100644 awx/ui/tests/spec/paginate/paginate.directive-test.js create mode 100644 awx/ui/tests/spec/smart-search/queryset.service-test.js create mode 100644 awx/ui/tests/spec/smart-search/smart-search.directive-test.js diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index dfa53e0d25..d554ce8cc0 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -729,64 +729,7 @@ legend { .navigation { margin: 15px 0 15px 0; } - -.page-number { - display: inline-block; - padding: 0; - margin: 0; -} - -.page-number-small { - display: inline-block; - margin-left: 10px; - font-size: 11px; -} - -/* Pagination */ - .page-label { - font-size: 12px; - margin-top: 0; - text-align: right; - } - - .pagination { - margin-top: 0; - margin-bottom: 7px; - } - - .pagination>li>a, - .pagination>li>span { - border: 1px solid @grey-border; - padding: 3px 6px; - font-size: 10px; - } - - .pagination li { - a#next-page { - border-radius: 0 4px 4px 0; - } - - a#previous-page { - border-radius: 4px 0 0 4px; - } - } - .modal-body { - .pagination { - margin-top: 15px; - margin-bottom: 0; - } - .pagination > li > a { - border: none; - padding-top: 0; - padding-bottom: 0; - } - .pagination > .active > a { - background-color: @default-bg; - color: #428bca; - border-color: none; - border: 1px solid @default-link; - } .alert { padding: 0; border: none; @@ -1623,6 +1566,10 @@ a.btn-disabled:hover { /* Sort link styles */ +.list-header-noSort:hover.list-header:hover{ + cursor: default; +} + .list-header:hover { cursor: pointer; } diff --git a/awx/ui/client/legacy-styles/jobs.less b/awx/ui/client/legacy-styles/jobs.less index 0ae748d0b4..86e14926e1 100644 --- a/awx/ui/client/legacy-styles/jobs.less +++ b/awx/ui/client/legacy-styles/jobs.less @@ -19,13 +19,7 @@ } .job-list { - .pagination li { - } - .pagination li a { - font-size: 12px; - padding: 3px 6px; - } i[class*="icon-job-"] { font-size: 13px; } diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 098f936ab1..763694f6d2 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -116,44 +116,6 @@ table, tbody { margin-left: 15px; } -/* -- Pagination -- */ -.List-pagination { - margin-top: 20px; - font-size: 12px; - color: @list-pagin-text; - text-transform: uppercase; - height: 22px; - display: flex; -} - -.List-paginationPagerHolder { - display: flex; - flex: 1 0 auto; -} - -.List-paginationPager { - display: flex; -} - -.List-paginationPager--pageof { - line-height: 22px; - margin-left: 10px; -} - -.List-paginationPager--item { - border-color: @list-pagin-bord; -} - -.List-paginationPager--active { - border-color: @list-pagin-bord-act!important; - background-color: @list-pagin-bg-act!important; -} - -.List-paginationItemsOf { - display: flex; - justify-content: flex-end; -} - .List-header { display: flex; min-height: 34px; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js index 75177cc308..d5774e7c79 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js +++ b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js @@ -11,23 +11,12 @@ * Controller for handling permissions adding */ -export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function (rootScope, scope, GetBasePath, Rest, $q, Wait, ProcessErrors) { - var manuallyUpdateChecklists = function(list, id, isSelected) { - var elemScope = angular - .element("#" + - list + "s_table #" + id + ".List-tableRow input") - .scope(); - if (elemScope) { - elemScope.isSelected = !!isSelected; - } - }; +export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function(rootScope, scope, GetBasePath, Rest, $q, Wait, ProcessErrors) { scope.allSelected = []; // the object permissions are being added to - scope.object = scope[scope.$parent.list - .iterator + "_obj"]; - + scope.object = scope.resourceData.data; // array for all possible roles for the object scope.roles = Object .keys(scope.object.summary_fields.object_roles) @@ -36,7 +25,8 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr value: scope.object.summary_fields .object_roles[key].id, label: scope.object.summary_fields - .object_roles[key].name }; + .object_roles[key].name + }; }); // TODO: get working with api @@ -48,7 +38,8 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr name: scope.object.summary_fields .object_roles[key].name, description: scope.object.summary_fields - .object_roles[key].description }; + .object_roles[key].description + }; }); scope.showKeyPane = false; @@ -63,90 +54,44 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr scope.teamsSelected = !scope.usersSelected; }; - // manually handle selection/deselection of user/team checkboxes - scope.$on("selectedOrDeselected", function(e, val) { - val = val.value; - if (val.isSelected) { - // deselected, so remove from the allSelected list - scope.allSelected = scope.allSelected.filter(function(i) { - // return all but the object who has the id and type - // of the element to deselect - return (!(val.id === i.id && val.type === i.type)); - }); + // pop/push into unified collection of selected users & teams + scope.$on("selectedOrDeselected", function(e, value) { + let item = value.value; + + function buildName(user) { + return (user.first_name && + user.last_name) ? + user.first_name + " " + + user.last_name : + user.username; + } + + if (item.isSelected) { + if (item.type === 'user') { + item.name = buildName(item); + } + scope.allSelected.push(item); } else { - // selected, so add to the allSelected list - var getName = function(val) { - if (val.type === "user") { - return (val.first_name && - val.last_name) ? - val.first_name + " " + - val.last_name : - val.username; - } else { - return val.name; - } - }; - scope.allSelected.push({ - name: getName(val), - type: val.type, - roles: [], - id: val.id - }); + scope.allSelected = _.remove(scope.allSelected, { id: item.id }); } }); - // used to handle changes to the itemsSelected scope var on "next page", - // "sorting etc." - scope.$on("itemsSelected", function(e, inList) { - // compile a list of objects that needed to be checked in the lists - scope.updateLists = scope.allSelected.filter(function(inMemory) { - var notInList = true; - inList.forEach(function(val) { - // if the object is part of the allSelected list and is - // selected, - // you don't need to add it updateLists - if (inMemory.id === val.id && - inMemory.type === val.type) { - notInList = false; - } - }); - return notInList; - }); - }); - - // handle changes to the updatedLists by manually selected those values in - // the UI - scope.$watch("updateLists", function(toUpdate) { - (toUpdate || []).forEach(function(obj) { - manuallyUpdateChecklists(obj.type, obj.id, true); - }); - - delete scope.updateLists; - }); - - // remove selected user/team - scope.removeObject = function(obj) { - manuallyUpdateChecklists(obj.type, obj.id, false); - - scope.allSelected = scope.allSelected.filter(function(i) { - return (!(obj.id === i.id && obj.type === i.type)); - }); - }; - // update post url list scope.$watch("allSelected", function(val) { scope.posts = _ .flatten((val || []) - .map(function (owner) { - var url = GetBasePath(owner.type + "s") + owner.id + - "/roles/"; + .map(function(owner) { + var url = GetBasePath(owner.type + "s") + owner.id + + "/roles/"; - return (owner.roles || []) - .map(function (role) { - return {url: url, - id: role.value}; - }); - })); + return (owner.roles || []) + .map(function(role) { + return { + url: url, + id: role.value + }; + }); + })); }, true); // post roles to api @@ -156,22 +101,22 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr var requests = scope.posts .map(function(post) { Rest.setUrl(post.url); - return Rest.post({"id": post.id}); + return Rest.post({ "id": post.id }); }); $q.all(requests) - .then(function () { + .then(function() { Wait('stop'); rootScope.$broadcast("refreshList", "permission"); scope.closeModal(); - }, function (error) { + }, function(error) { Wait('stop'); rootScope.$broadcast("refreshList", "permission"); scope.closeModal(); ProcessErrors(null, error.data, error.status, null, { hdr: 'Error!', msg: 'Failed to post role(s): POST returned status' + - error.status + error.status }); }); }; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js index 57bb658788..284110b0ce 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js +++ b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js @@ -6,55 +6,28 @@ import addPermissionsController from './addPermissions.controller'; /* jshint unused: vars */ -export default - [ 'templateUrl', - 'Wait', - function(templateUrl, Wait) { - return { - restrict: 'E', - scope: true, - controller: addPermissionsController, - templateUrl: templateUrl('access/addPermissions/addPermissions'), - link: function(scope, element, attrs, ctrl) { - scope.withoutTeamPermissions = attrs.withoutTeamPermissions; - scope.toggleFormTabs('users'); +export default ['templateUrl', '$state', + 'Wait', 'addPermissionsUsersList', 'addPermissionsTeamsList', + function(templateUrl, $state, Wait, usersList, teamsList) { + return { + restrict: 'E', + scope: { + usersDataset: '=', + teamsDataset: '=', + resourceData: '=', + }, + controller: addPermissionsController, + templateUrl: templateUrl('access/addPermissions/addPermissions'), + link: function(scope, element, attrs) { + scope.toggleFormTabs('users'); + $('#add-permissions-modal').modal('show'); - $("body").addClass("is-modalOpen"); + scope.closeModal = function() { + $state.go('^', null, {reload: true}); + }; - $("body").append(element); - - Wait('start'); - - - scope.$broadcast("linkLists"); - - setTimeout(function() { - $('#add-permissions-modal').modal("show"); - }, 200); - - $('.modal[aria-hidden=false]').each(function () { - if ($(this).attr('id') !== 'add-permissions-modal') { - $(this).modal('hide'); - } - }); - - scope.closeModal = function() { - $("body").removeClass("is-modalOpen"); - $('#add-permissions-modal').on('hidden.bs.modal', - function () { - $('.AddPermissions').remove(); - }); - $('#add-permissions-modal').modal('hide'); - }; - - scope.$on('closePermissionsModal', function() { - scope.closeModal(); - }); - - Wait('stop'); - - window.scrollTo(0,0); - } - }; - } - ]; + window.scrollTo(0, 0); + } + }; + } +]; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html index cc2f7ee0c7..264a4cf834 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -45,13 +45,11 @@ -
- - +
+
-
- - +
+
", - link: function(scope, element, attrs, ctrl) { - scope.$on("linkLists", function(e) { - var generator = generateList, - list = addPermissionsTeamsList, - url = GetBasePath("teams"), - set = "teams", - id = "addPermissionsTeamsList", - mode = "edit"; +export default ['addPermissionsTeamsList', 'addPermissionsUsersList', '$compile', 'generateList', 'GetBasePath', 'SelectionInit', function(addPermissionsTeamsList, + addPermissionsUsersList, $compile, generateList, + GetBasePath, SelectionInit) { + return { + restrict: 'E', + scope: { + allSelected: '=', + view: '@', + dataset: '=' + }, + template: "
", + link: function(scope, element, attrs, ctrl) { + let listMap, list, list_html; - if (attrs.type === 'users') { - list = addPermissionsUsersList; - url = GetBasePath("users") + "?is_superuser=false"; - set = "users"; - id = "addPermissionsUsersList"; - mode = "edit"; - } + listMap = {Teams: addPermissionsTeamsList, Users: addPermissionsUsersList}; + list = listMap[scope.view]; + list_html = generateList.build({ + mode: 'edit', + list: list + }); - scope.id = id; + scope.list = listMap[scope.view]; + scope[`${list.iterator}_dataset`] = scope.dataset.data; + scope[`${list.name}`] = scope[`${list.iterator}_dataset`].results; - scope.$watch("selectedItems", function() { - scope.$emit("itemsSelected", scope.selectedItems); - }); + scope.$watch(list.name, function(){ + _.forEach(scope[`${list.name}`], isSelected); + }); - element.find(".addPermissionsList-inner") - .attr("id", id); - - generator.inject(list, { id: id, - title: false, mode: mode, scope: scope }); - - SearchInit({ scope: scope, set: set, - list: list, url: url }); - - PaginateInit({ scope: scope, - list: list, url: url, pageSize: 5 }); - - scope.search(list.iterator); - }); + function isSelected(item){ + if(_.find(scope.allSelected, {id: item.id})){ + item.isSelected = true; } - }; + return item; + } + element.append(list_html); + $compile(element.contents())(scope); } - ]; + }; +}]; diff --git a/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js index 5c0513c8db..8955d30aa0 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js +++ b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js @@ -7,9 +7,14 @@ export default function() { return { - searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', name: 'users', iterator: 'user', + defaultSearchParams: function(term){ + return {or__username__icontains: term, + or__first_name__icontains: term, + or__last_name__icontains: term + }; + }, title: false, listTitleBadge: false, multiSelect: true, @@ -17,7 +22,6 @@ index: false, hover: true, emptyListText : 'No Users exist', - fields: { first_name: { label: 'First Name', diff --git a/awx/ui/client/src/activity-stream/activitystream.controller.js b/awx/ui/client/src/activity-stream/activitystream.controller.js index 0c16f304b7..05609d3bc5 100644 --- a/awx/ui/client/src/activity-stream/activitystream.controller.js +++ b/awx/ui/client/src/activity-stream/activitystream.controller.js @@ -8,22 +8,31 @@ * @ngdoc function * @name controllers.function:Activity Stream * @description This controller controls the activity stream. -*/ -function activityStreamController($scope, $state, subTitle, Stream, GetTargetTitle) { + */ +function activityStreamController($scope, $state, subTitle, Stream, GetTargetTitle, list, Dataset) { - // subTitle is passed in via a resolve on the route. If there is no subtitle - // generated in the resolve then we go get the targets generic title. + init(); - // Get the streams sub-title based on the target. This scope variable is leveraged - // when we define the activity stream list. Specifically it is included in the list - // title. - $scope.streamSubTitle = subTitle ? subTitle : GetTargetTitle($state.params.target); + function init() { + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - // Open the stream - Stream({ - scope: $scope - }); + // subTitle is passed in via a resolve on the route. If there is no subtitle + // generated in the resolve then we go get the targets generic title. + + // Get the streams sub-title based on the target. This scope variable is leveraged + // when we define the activity stream list. Specifically it is included in the list + // title. + $scope.streamSubTitle = subTitle ? subTitle : GetTargetTitle($state.params.target); + + // Open the stream + Stream({ + scope: $scope + }); + } } -export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle', activityStreamController]; +export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle', 'StreamList', 'Dataset', activityStreamController]; diff --git a/awx/ui/client/src/activity-stream/activitystream.partial.html b/awx/ui/client/src/activity-stream/activitystream.partial.html deleted file mode 100644 index 8c6263b11d..0000000000 --- a/awx/ui/client/src/activity-stream/activitystream.partial.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/awx/ui/client/src/activity-stream/activitystream.route.js b/awx/ui/client/src/activity-stream/activitystream.route.js index 4da5343651..3fa3d961c9 100644 --- a/awx/ui/client/src/activity-stream/activitystream.route.js +++ b/awx/ui/client/src/activity-stream/activitystream.route.js @@ -4,49 +4,73 @@ * All Rights Reserved *************************************************/ - import {templateUrl} from '../shared/template-url/template-url.factory'; - export default { name: 'activityStream', route: '/activity_stream?target&id', - templateUrl: templateUrl('activity-stream/activitystream'), - controller: 'activityStreamController', + searchPrefix: 'activity', data: { activityStream: true }, + params: { + activity_search: { + value: { + // default params will not generate search tags + order_by: '-timestamp', + or__object1: null, + or__object2: null + } + } + }, ncyBreadcrumb: { label: "ACTIVITY STREAM" }, - onExit: function(){ + onExit: function() { $('#stream-detail-modal').modal('hide'); $('.modal-backdrop').remove(); $('body').removeClass('modal-open'); }, - resolve: { - features: ['FeaturesService', 'ProcessErrors', '$state', '$rootScope', - function(FeaturesService, ProcessErrors, $state, $rootScope) { - var features = FeaturesService.get(); - if(features){ - if(FeaturesService.featureEnabled('activity_streams')) { - return features; - } - else { - $state.go('dashboard'); - } + views: { + 'list@': { + controller: 'activityStreamController', + templateProvider: function(StreamList, generateList) { + let html = generateList.build({ + list: StreamList, + mode: 'edit' + }); + html = generateList.wrapPanel(html); + return html; } - $rootScope.featuresConfigured.promise.then(function(features){ - if(features){ - if(FeaturesService.featureEnabled('activity_streams')) { + } + }, + resolve: { + Dataset: ['StreamList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + features: ['FeaturesService', 'ProcessErrors', '$state', '$rootScope', + function(FeaturesService, ProcessErrors, $state, $rootScope) { + var features = FeaturesService.get(); + if (features) { + if (FeaturesService.featureEnabled('activity_streams')) { return features; - } - else { + } else { $state.go('dashboard'); } } - }); - }], - subTitle: - [ '$stateParams', + $rootScope.featuresConfigured.promise.then(function(features) { + if (features) { + if (FeaturesService.featureEnabled('activity_streams')) { + return features; + } else { + $state.go('dashboard'); + } + } + }); + } + ], + subTitle: ['$stateParams', 'Rest', 'ModelToBasePathKey', 'GetBasePath', @@ -65,15 +89,14 @@ export default { .then(function(data) { // Return the name or the username depending on which is available. return (data.data.name || data.data.username); - }).catch(function (response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get title info. GET returned status: ' + - response.status + }).catch(function(response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get title info. GET returned status: ' + + response.status + }); }); - }); - } - else { + } else { return null; } } diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index 229c3d4271..8e01af25e5 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -10,7 +10,7 @@ export default ['templateUrl', function(templateUrl) { scope: true, replace: true, templateUrl: templateUrl('activity-stream/streamDropdownNav/stream-dropdown-nav'), - controller: ['$scope', '$state', 'CreateSelect2', function($scope, $state, CreateSelect2) { + controller: ['$scope', '$state', '$stateParams','CreateSelect2', function($scope, $state, $stateParams, CreateSelect2) { $scope.streamTarget = ($state.params && $state.params.target) ? $state.params.target : 'dashboard'; @@ -35,14 +35,17 @@ export default ['templateUrl', function(templateUrl) { }); $scope.changeStreamTarget = function(){ - if($scope.streamTarget && $scope.streamTarget === 'dashboard') { // Just navigate to the base activity stream - $state.go('activityStream', {}, {inherit: false}); + $state.go('activityStream'); } else { + let search = _.merge($stateParams.activity_search, { + or__object1: $scope.streamTarget, + or__object2: $scope.streamTarget + }); // Attach the taget to the query parameters - $state.go('activityStream', {target: $scope.streamTarget}, {inherit: false}); + $state.go('activityStream', {target: $scope.streamTarget, activity_search: search}); } }; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index b7a3c25d43..7b0de854c3 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -15,6 +15,12 @@ import 'jquery.resize'; import 'codemirror'; import 'js-yaml'; import 'select2'; +import uiRouter from 'angular-ui-router'; +// backwards compatibility for $stateChange* events +import 'angular-ui-router/release/stateEvents'; +// ui-router debugging +//import { trace } from 'angular-ui-router'; +//trace.enable(); // Configuration dependencies global.$AnsibleConfig = null; @@ -27,7 +33,7 @@ if ($basePath) { // Modules import './helpers'; -import './forms'; +import * as forms from './forms'; import './lists'; import './widgets'; import './filters'; @@ -40,12 +46,10 @@ import systemTracking from './system-tracking/main'; import inventories from './inventories/main'; import inventoryScripts from './inventory-scripts/main'; import organizations from './organizations/main'; -import permissions from './permissions/main'; import managementJobs from './management-jobs/main'; import jobDetail from './job-detail/main'; import jobSubmission from './job-submission/main'; import notifications from './notifications/main'; -import access from './access/main'; import about from './about/main'; import license from './license/main'; import setupMenu from './setup-menu/main'; @@ -54,23 +58,17 @@ import breadCrumb from './bread-crumb/main'; import browserData from './browser-data/main'; import dashboard from './dashboard/main'; import moment from './shared/moment/main'; -import templateUrl from './shared/template-url/main'; import login from './login/main'; import activityStream from './activity-stream/main'; import standardOut from './standard-out/main'; import JobTemplates from './job-templates/main'; -import search from './search/main'; import credentials from './credentials/main'; import { ProjectsList, ProjectsAdd, ProjectsEdit } from './controllers/Projects'; -import OrganizationsList from './organizations/list/organizations-list.controller'; -import OrganizationsAdd from './organizations/add/organizations-add.controller'; import { UsersList, UsersAdd, UsersEdit } from './controllers/Users'; import { TeamsList, TeamsAdd, TeamsEdit } from './controllers/Teams'; import RestServices from './rest/main'; -import './lookup/main'; -import './shared/api-loader'; -import './shared/form-generator'; +import access from './access/main'; import './shared/Modal'; import './shared/prompt-dialog'; import './shared/directives'; @@ -80,7 +78,7 @@ import config from './shared/config/main'; import './login/authenticationServices/pendo/ng-pendo'; import footer from './footer/main'; import scheduler from './scheduler/main'; -import {N_} from './i18n'; +import { N_ } from './i18n'; var tower = angular.module('Tower', [ // how to add CommonJS / AMD third-party dependencies: @@ -88,17 +86,17 @@ var tower = angular.module('Tower', [ // 2. add package name to ./grunt-tasks/webpack.vendorFiles require('angular-breadcrumb'), require('angular-codemirror'), - require('angular-cookies'), require('angular-drag-and-drop-lists'), - require('angular-ui-router'), require('angular-sanitize'), require('angular-scheduler').name, require('angular-tz-extensions'), require('lr-infinite-scroll'), require('ng-toast'), - + uiRouter, + 'ui.router.state.events', about.name, + access.name, license.name, RestServices.name, browserData.name, @@ -106,14 +104,13 @@ var tower = angular.module('Tower', [ inventories.name, inventoryScripts.name, organizations.name, - permissions.name, + //permissions.name, managementJobs.name, setupMenu.name, mainMenu.name, breadCrumb.name, dashboard.name, moment.name, - templateUrl.name, login.name, activityStream.name, footer.name, @@ -121,27 +118,19 @@ var tower = angular.module('Tower', [ jobSubmission.name, notifications.name, standardOut.name, - access.name, JobTemplates.name, portalMode.name, - search.name, config.name, credentials.name, //'templates', 'Utilities', 'OrganizationFormDefinition', 'UserFormDefinition', - 'FormGenerator', 'OrganizationListDefinition', 'jobTemplates', 'UserListDefinition', 'UserHelper', 'PromptDialog', - 'ApiLoader', - 'RelatedSearchHelper', - 'SearchHelper', - 'PaginationHelpers', - 'RefreshHelper', 'AWDirectives', 'InventoriesListDefinition', 'InventoryFormDefinition', @@ -161,7 +150,6 @@ var tower = angular.module('Tower', [ 'TeamHelper', 'CredentialsListDefinition', 'CredentialFormDefinition', - 'LookUpHelper', 'JobTemplatesListDefinition', 'PortalJobTemplatesListDefinition', 'JobTemplateFormDefinition', @@ -223,10 +211,12 @@ var tower = angular.module('Tower', [ timeout: 4000 }); }]) - .config(['$stateProvider', '$urlRouterProvider', '$breadcrumbProvider', - '$urlMatcherFactoryProvider', - function($stateProvider, $urlRouterProvider, $breadcrumbProvider, - $urlMatcherFactoryProvider) { + .config(['$urlRouterProvider', '$breadcrumbProvider', 'QuerySetProvider', + '$urlMatcherFactoryProvider', 'stateDefinitionsProvider', '$stateProvider', '$stateExtenderProvider', + function($urlRouterProvider, $breadcrumbProvider, QuerySet, + $urlMatcherFactoryProvider, stateDefinitionsProvider, $stateProvider, $stateExtenderProvider) { + let $stateExtender = $stateExtenderProvider.$get(), + stateDefinitions = stateDefinitionsProvider.$get(); $urlMatcherFactoryProvider.strictMode(false); $breadcrumbProvider.setOptions({ templateUrl: urlPrefix + 'partials/breadcrumb.html' @@ -234,202 +224,133 @@ var tower = angular.module('Tower', [ // route to the details pane of /job/:id/host-event/:eventId if no other child specified $urlRouterProvider.when('/jobs/*/host-event/*', '/jobs/*/host-event/*/details'); + $urlRouterProvider.otherwise('/home'); - // $urlRouterProvider.otherwise("/home"); - $urlRouterProvider.otherwise(function($injector) { - var $state = $injector.get("$state"); - $state.go('dashboard'); + $urlMatcherFactoryProvider.type('queryset', { + // encoding + // from {operator__key1__comparator=value, ... } + // to "_search=operator:key:compator=value& ... " + encode: function(item) { + return QuerySet.$get().encodeArr(item); + }, + // decoding + // from "_search=operator:key:compator=value& ... " + // to "_search=operator:key:compator=value& ... " + decode: function(item) { + return QuerySet.$get().decodeArr(item); + }, + // directionality - are we encoding or decoding? + is: function(item) { + // true: encode to uri + // false: decode to $stateParam + return angular.isObject(item); + } }); - /* Mark translatable strings with N_() and - * extract them by 'grunt nggettext_extract' - * but angular.config() cannot get gettextCatalog. - */ - $stateProvider. - state('teams', { - url: '/teams', - templateUrl: urlPrefix + 'partials/teams.html', - controller: TeamsList, - data: { - activityStream: true, - activityStreamTarget: 'team' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_("TEAMS") - } - }). - state('teams.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/teams.html', - controller: TeamsAdd, - ncyBreadcrumb: { - parent: "teams", - label: N_("CREATE TEAM") - } - }). + // Handy hook for debugging register/deregister of lazyLoad'd states + // $stateProvider.stateRegistry.onStatesChanged((event, states) =>{ + // console.log(event, states) + // }) - state('teams.edit', { - url: '/:team_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: TeamsEdit, - data: { - activityStreamId: 'team_id' - }, - ncyBreadcrumb: { - parent: "teams", - label: "{{team_obj.name}}" - } - }). - state('teamUsers', { - url: '/teams/:team_id/users', - templateUrl: urlPrefix + 'partials/teams.html', - controller: UsersList - }). + // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry + // see: stateDefinition.factory for usage documentation + $stateProvider.state({ + name: 'projects', + url: '/projects', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'projects', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'ProjectList', + form: 'ProjectsForm', + controllers: { + list: ProjectsList, // DI strings or objects + add: ProjectsAdd, + edit: ProjectsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'project', + socket: { + "groups": { + "jobs": ["status_changed"] + } + } + } + }) + }); - state('teamUserEdit', { - url: '/teams/:team_id/users/:user_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: UsersEdit - }). - - state('teamProjects', { - url: '/teams/:team_id/projects', - templateUrl: urlPrefix + 'partials/teams.html', - controller: ProjectsList - }). - - state('teamProjectAdd', { - url: '/teams/:team_id/projects/add', - templateUrl: urlPrefix + 'partials/teams.html', - controller: ProjectsAdd - }). - - state('teamProjectEdit', { - url: '/teams/:team_id/projects/:project_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: ProjectsEdit - }). - - state('teamCredentials', { - url: '/teams/:team_id/credentials', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsList - }). - - state('teamCredentialAdd', { - url: '/teams/:team_id/credentials/add', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsAdd - }). - - state('teamCredentialEdit', { - url: '/teams/:team_id/credentials/:credential_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsEdit - }). - - state('credentials', { + $stateProvider.state({ + name: 'credentials', url: '/credentials', - templateUrl: urlPrefix + 'partials/credentials.html', - controller: CredentialsList, - data: { - activityStream: true, - activityStreamTarget: 'credential' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_("CREDENTIALS") - } - }). + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'credentials', + modes: ['add', 'edit'], + list: 'CredentialList', + form: 'CredentialForm', + controllers: { + list: CredentialsList, + add: CredentialsAdd, + edit: CredentialsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'credential' + }, + ncyBreadcrumb: { + parent: 'setup', + label: 'CREDENTIALS' + } + }) + }); - state('credentials.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/credentials.html', - controller: CredentialsAdd, - ncyBreadcrumb: { - parent: "credentials", - label: N_("CREATE CREDENTIAL") - } - }). + $stateProvider.state({ + name: 'teams', + url: '/teams', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'teams', + modes: ['add', 'edit'], + list: 'TeamList', + form: 'TeamForm', + controllers: { + list: TeamsList, + add: TeamsAdd, + edit: TeamsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'team' + }, + ncyBreadcrumb: { + parent: 'setup', + label: 'TEAMS' + } + }) + }); - state('credentials.edit', { - url: '/:credential_id', - templateUrl: urlPrefix + 'partials/credentials.html', - controller: CredentialsEdit, - data: { - activityStreamId: 'credential_id' - }, - ncyBreadcrumb: { - parent: "credentials", - label: "{{credential_obj.name}}" - } - }). - - state('users', { + $stateProvider.state({ + name: 'users', url: '/users', - templateUrl: urlPrefix + 'partials/users.html', - controller: UsersList, - data: { - activityStream: true, - activityStreamTarget: 'user' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_("USERS") - } - }). - - state('users.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/users.html', - controller: UsersAdd, - ncyBreadcrumb: { - parent: "users", - label: N_("CREATE USER") - } - }). - - state('users.edit', { - url: '/:user_id', - templateUrl: urlPrefix + 'partials/users.html', - controller: UsersEdit, - data: { - activityStreamId: 'user_id' - }, - ncyBreadcrumb: { - parent: "users", - label: "{{user_obj.username}}" - } - }). - - state('userCredentials', { - url: '/users/:user_id/credentials', - templateUrl: urlPrefix + 'partials/users.html', - controller: CredentialsList - }). - - state('userCredentialAdd', { - url: '/users/:user_id/credentials/add', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsAdd - }). - - state('teamUserCredentialEdit', { - url: '/teams/:user_id/credentials/:credential_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsEdit - }). - - state('sockets', { - url: '/sockets', - templateUrl: urlPrefix + 'partials/sockets.html', - controller: SocketsController, - ncyBreadcrumb: { - label: N_("SOCKETS") - } + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'users', + modes: ['add', 'edit'], + list: 'UserList', + form: 'UserForm', + controllers: { + list: UsersList, + add: UsersAdd, + edit: UsersEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'user' + }, + ncyBreadcrumb: { + parent: 'setup', + label: 'USERS' + } + }) }); } ]) @@ -447,17 +368,23 @@ var tower = angular.module('Tower', [ }]); }]) -.run(['$stateExtender', '$q', '$compile', '$cookieStore', '$rootScope', '$log', +.run(['$stateExtender', '$q', '$compile', '$cookieStore', '$rootScope', '$log', '$stateParams', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService', 'FeaturesService', '$filter', 'SocketService', 'I18NInit', - function($stateExtender, $q, $compile, $cookieStore, $rootScope, $log, + function($stateExtender, $q, $compile, $cookieStore, $rootScope, $log, $stateParams, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, LoadConfig, Store, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService, $filter, SocketService, I18NInit) { + $rootScope.$state = $state; + $rootScope.$state.matches = function(stateName) { + return $state.current.name.search(stateName) > 0; + }; + $rootScope.$stateParams = $stateParams; + I18NInit(); $stateExtender.addState({ name: 'dashboard', @@ -465,14 +392,14 @@ var tower = angular.module('Tower', [ templateUrl: urlPrefix + 'partials/home.html', controller: Home, params: { licenseMissing: null }, - socket: { - "groups":{ - "jobs": ["status_changed"] - } - }, data: { activityStream: true, - refreshButton: true + refreshButton: true, + socket: { + "groups": { + "jobs": ["status_changed"] + } + }, }, ncyBreadcrumb: { label: N_("DASHBOARD") @@ -491,94 +418,94 @@ var tower = angular.module('Tower', [ }); $stateExtender.addState({ + searchPrefix: 'job', name: 'jobs', url: '/jobs', - templateUrl: urlPrefix + 'partials/jobs.html', - controller: JobsListController, ncyBreadcrumb: { label: N_("JOBS") }, params: { - search: { - value: {order_by:'-finished'} + job_search: { + value: { order_by: '-finished' } } }, - socket: { - "groups":{ - "jobs": ["status_changed"], - "schedules": ["changed"] - } - } - }); - - $stateExtender.addState({ - name: 'projects', - url: '/projects?{status}', - templateUrl: urlPrefix + 'partials/projects.html', - controller: ProjectsList, data: { - activityStream: true, - activityStreamTarget: 'project' + socket: { + "groups": { + "jobs": ["status_changed"], + "schedules": ["changed"] + } + } }, - ncyBreadcrumb: { - label: N_("PROJECTS") + resolve: { + Dataset: ['AllJobsList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + }] }, - socket: { - "groups":{ - "jobs": ["status_changed"] + views: { + 'list@': { + templateUrl: urlPrefix + 'partials/jobs.html', + }, + 'list@jobs': { + templateProvider: function(AllJobsList, generateList) { + let html = generateList.build({ + list: AllJobsList, + mode: 'edit' + }); + return html; + }, + controller: JobsListController } } }); + $stateExtender.addState({ - name: 'projects.add', - url: '/add', - templateUrl: urlPrefix + 'partials/projects.html', - controller: ProjectsAdd, - ncyBreadcrumb: { - parent: "projects", - label: N_("CREATE PROJECT") - }, - socket: { - "groups":{ - "jobs": ["status_changed"] - } + name: 'teamUsers', + url: '/teams/:team_id/users', + templateUrl: urlPrefix + 'partials/teams.html', + controller: UsersList, + resolve: { + Users: ['UsersList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + }] } }); + $stateExtender.addState({ - name: 'projects.edit', - url: '/:id', - templateUrl: urlPrefix + 'partials/projects.html', - controller: ProjectsEdit, - data: { - activityStreamId: 'id' - }, + name: 'userCredentials', + url: '/users/:user_id/credentials', + templateUrl: urlPrefix + 'partials/users.html', + controller: CredentialsList + }); + + $stateExtender.addState({ + name: 'userCredentialAdd', + url: '/users/:user_id/credentials/add', + templateUrl: urlPrefix + 'partials/teams.html', + controller: CredentialsAdd + }); + + $stateExtender.addState({ + name: 'teamUserCredentialEdit', + url: '/teams/:user_id/credentials/:credential_id', + templateUrl: urlPrefix + 'partials/teams.html', + controller: CredentialsEdit + }); + + $stateExtender.addState({ + name: 'sockets', + url: '/sockets', + templateUrl: urlPrefix + 'partials/sockets.html', + controller: SocketsController, ncyBreadcrumb: { - parent: 'projects', - label: '{{name}}' - }, - socket: { - "groups":{ - "jobs": ["status_changed"] - } + label: 'SOCKETS' } }); - $stateExtender.addState({ - name: 'projectOrganizations', - url: '/projects/:project_id/organizations', - templateUrl: urlPrefix + 'partials/projects.html', - controller: OrganizationsList - }); - - $stateExtender.addState({ - name: 'projectOrganizationAdd', - url: '/projects/:project_id/organizations/add', - templateUrl: urlPrefix + 'partials/projects.html', - controller: OrganizationsAdd - }); - $rootScope.addPermission = function(scope) { $compile("")(scope); }; @@ -604,7 +531,7 @@ var tower = angular.module('Tower', [ Rest.post({ "disassociate": true, "id": entry.id }) .success(function() { Wait('stop'); - $rootScope.$broadcast("refreshList", "permission"); + $state.go('.', null, { reload: true }); }) .error(function(data, status) { ProcessErrors($rootScope, data, status, null, { diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index cfc6630412..36fe3901ee 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -25,18 +25,29 @@ export default if(streamConfig && streamConfig.activityStream) { if(streamConfig.activityStreamTarget) { stateGoParams.target = streamConfig.activityStreamTarget; + stateGoParams.activity_search = { + or__object1: streamConfig.activityStreamTarget, + or__object2: streamConfig.activityStreamTarget, + order_by: '-timestamp', + page_size: '20', + }; + } + else { + stateGoParams.activity_search = { + order_by: '-timestamp', + page_size: '20', + }; } if(streamConfig.activityStreamId) { stateGoParams.id = $state.params[streamConfig.activityStreamId]; } + } originalRoute = $state.current; $state.go('activityStream', stateGoParams); } // The user is navigating away from the activity stream - take them back from whence they came else { - // Pull the previous state out of local storage - if(originalRoute) { $state.go(originalRoute.name, originalRoute.fromParams); } @@ -51,14 +62,13 @@ export default }; scope.$on("$stateChangeStart", function updateActivityStreamButton(event, toState, toParams, fromState, fromParams) { - if(fromState && !Empty(fromState.name)) { // Go ahead and attach the from params to the state object so that it can all be stored together fromState.fromParams = fromParams ? fromParams : {}; // Store the state that we're coming from in local storage to be accessed when navigating away from the // activity stream - Store('previous_state', fromState); + //Store('previous_state', fromState); } streamConfig = (toState && toState.data) ? toState.data : {}; diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 11e2b4e435..f8ebbf2197 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -8,114 +8,63 @@ * @ngdoc function * @name controllers.function:Credentials * @description This controller's for the credentials page -*/ + */ export function CredentialsList($scope, $rootScope, $location, $log, - $stateParams, Rest, Alert, CredentialList, GenerateList, Prompt, SearchInit, - PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, - SelectionInit, GetChoices, Wait, $state, $filter, rbacUiControlService) { + $stateParams, Rest, Alert, CredentialList, Prompt, ClearScope, + ProcessErrors, GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset) { + ClearScope(); - rbacUiControlService.canAdd('credentials') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - Wait('start'); - var list = CredentialList, - defaultUrl = GetBasePath('credentials'), - view = GenerateList, - base = $location.path().replace(/^\//, '').split('/')[0], - mode = (base === 'credentials') ? 'edit' : 'select', - url; + defaultUrl = GetBasePath('credentials'); - view.inject(list, { mode: mode, scope: $scope }); + init(); - $scope.selected = []; - $scope.credentialLoading = true; + function init() { + rbacUiControlService.canAdd('credentials') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); - url = GetBasePath(base) + ( (base === 'users') ? $stateParams.user_id + '/credentials/' : $stateParams.team_id + '/credentials/' ); - if (mode === 'select') { - SelectionInit({ scope: $scope, list: list, url: url, returnToCaller: 1 }); + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $scope.selected = []; } - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - var i, j; - - // Cleanup after a delete - Wait('stop'); - $('#prompt-modal').modal('hide'); - - list.fields.kind.searchOptions = $scope.credential_kind_options_list; - - // Translate the kind value - for (i = 0; i < $scope.credentials.length; i++) { - for (j = 0; j < $scope.credential_kind_options_list.length; j++) { - if ($scope.credential_kind_options_list[j].value === $scope.credentials[i].kind) { - $scope.credentials[i].kind = $scope.credential_kind_options_list[j].label; - break; - } - } - } - }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function () { - SearchInit({ - scope: $scope, - set: 'credentials', - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - $scope.search(list.iterator); - }); - - // Load the list of options for Kind - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'kind', - variable: 'credential_kind_options_list', - callback: 'choicesReadyCredential' - }); - - $scope.addCredential = function () { - $state.transitionTo('credentials.add'); + $scope.addCredential = function() { + $state.go('credentials.add'); }; - $scope.editCredential = function (id) { - $state.transitionTo('credentials.edit', {credential_id: id}); + $scope.editCredential = function(id) { + $state.go('credentials.edit', { credential_id: id }); }; - $scope.deleteCredential = function (id, name) { - var action = function () { + $scope.deleteCredential = function(id, name) { + var action = function() { $('#prompt-modal').modal('hide'); Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() - .success(function () { + .success(function() { + if (parseInt($state.params.credential_id) === id) { - $state.go("^", null, {reload: true}); + $state.go("^", null, { reload: true }); } else { - $scope.search(list.iterator); + // @issue: OLD SEARCH + // $scope.search(list.iterator); } }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); }); }; @@ -126,97 +75,74 @@ export function CredentialsList($scope, $rootScope, $location, $log, actionText: 'DELETE' }); }; - - $scope.$emit('choicesReadyCredential'); } CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log', - '$stateParams', 'Rest', 'Alert', 'CredentialList', 'generateList', 'Prompt', - 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'GetChoices', 'Wait', - '$state', '$filter', 'rbacUiControlService' + '$stateParams', 'Rest', 'Alert', 'CredentialList', 'Prompt', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset' ]; export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, - ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit, - LookUpInit, OrganizationList, GetBasePath, GetChoices, Empty, KindChange, + ClearScope, GetBasePath, GetChoices, Empty, KindChange, OwnerChange, FormSave, $state, CreateSelect2) { ClearScope(); // Inject dynamic view var form = CredentialForm, - generator = GenerateForm, defaultUrl = GetBasePath('credentials'), url; - $scope.keyEntered = false; - $scope.permissionsTooltip = 'Please save before assigning permissions'; - generator.inject(form, { mode: 'add', related: false, scope: $scope }); - generator.reset(); + init(); - // Load the list of options for Kind - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'kind', - variable: 'credential_kind_options' - }); + function init() { + // Load the list of options for Kind + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options' + }); - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'become_method', - variable: 'become_options' - }); + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); - CreateSelect2({ - element: '#credential_become_method', - multiple: false - }); + CreateSelect2({ + element: '#credential_become_method', + multiple: false + }); - CreateSelect2({ - element: '#credential_kind', - multiple: false - }); + CreateSelect2({ + element: '#credential_kind', + multiple: false + }); - $scope.canShareCredential = false; + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); - $rootScope.$watch('current_user', function(){ - try { - if ($rootScope.current_user.is_superuser) { - $scope.canShareCredential = true; - } else { - Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); - Rest.get() - .success(function(data) { - $scope.canShareCredential = (data.count) ? true : false; - }).error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); - }); - } + $scope.keyEntered = false; + $scope.permissionsTooltip = 'Please save before assigning permissions'; - - var orgUrl = ($rootScope.current_user.is_superuser) ? - GetBasePath("organizations") : - $rootScope.current_user.url + "admin_of_organizations?"; - - // Create LookUpInit for organizations - LookUpInit({ - scope: $scope, - url: orgUrl, - form: form, - list: OrganizationList, - field: 'organization', - input_type: 'radio', - autopopulateLookup: false - }); + // determine if the currently logged-in user may share this credential + // previous commentary said: "$rootScope.current_user isn't available because a call to the config endpoint hasn't finished resolving yet" + // I'm 99% sure this state's will never resolve block will be rejected if setup surrounding config endpoint hasn't completed + if ($rootScope.current_user && $rootScope.current_user.is_superuser) { + $scope.canShareCredential = true; + } else { + Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); + Rest.get() + .success(function(data) { + $scope.canShareCredential = (data.count) ? true : false; + }).error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); + }); } - catch(err){ - // $rootScope.current_user isn't available because a call to the config endpoint hasn't finished resolving yet - } - }); + } if (!Empty($stateParams.user_id)) { // Get the username based on incoming route @@ -226,10 +152,10 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, url = GetBasePath('users') + $stateParams.user_id + '/'; Rest.setUrl(url); Rest.get() - .success(function (data) { + .success(function(data) { $scope.user_username = data.username; }) - .error(function (data, status) { + .error(function(data, status) { ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user. GET status: ' + status }); }); } else if (!Empty($stateParams.team_id)) { @@ -240,10 +166,10 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, url = GetBasePath('teams') + $stateParams.team_id + '/'; Rest.setUrl(url); Rest.get() - .success(function (data) { + .success(function(data) { $scope.team_name = data.name; }) - .error(function (data, status) { + .error(function(data, status) { ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve team. GET status: ' + status }); }); } else { @@ -263,32 +189,30 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, }); // Handle Kind change - $scope.kindChange = function () { + $scope.kindChange = function() { KindChange({ scope: $scope, form: form, reset: true }); }; // Save - $scope.formSave = function () { - generator.clearApiErrors(); - generator.checkAutoFill(); + $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { FormSave({ scope: $scope, mode: 'add' }); } }; - $scope.formCancel = function () { - $state.transitionTo('credentials'); + $scope.formCancel = function() { + $state.go('credentials'); }; // Password change - $scope.clearPWConfirm = function (fld) { + $scope.clearPWConfirm = function(fld) { // If password value changes, make sure password_confirm must be re-entered $scope[fld] = ''; $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); }; // Respond to 'Ask at runtime?' checkbox - $scope.ask = function (fld, associated) { + $scope.ask = function(fld, associated) { if ($scope[fld + '_ask']) { $scope[fld] = 'ASK'; $("#" + form.name + "_" + fld + "_input").attr("type", "text"); @@ -313,7 +237,7 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, }; // Click clear button - $scope.clear = function (fld, associated) { + $scope.clear = function(fld, associated) { $scope[fld] = ''; $scope[associated] = ''; $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); @@ -324,54 +248,88 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList', - 'SearchInit', 'PaginateInit', 'LookUpInit', 'OrganizationList', - 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'OwnerChange', - 'FormSave', '$state', 'CreateSelect2' + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', + 'OwnerChange', 'FormSave', '$state', 'CreateSelect2' ]; - export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, - $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, - RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, Prompt, - GetBasePath, GetChoices, KindChange, OrganizationList, LookUpInit, Empty, - OwnerChange, FormSave, Wait, $state, CreateSelect2, Authorization) { - if (!$rootScope.current_user) { - Authorization.restoreUserInfo(); - } + $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, + GetBasePath, GetChoices, KindChange, Empty, OwnerChange, FormSave, Wait, + $state, CreateSelect2, Authorization) { ClearScope(); var defaultUrl = GetBasePath('credentials'), - generator = GenerateForm, form = CredentialForm, base = $location.path().replace(/^\//, '').split('/')[0], master = {}, - id = $stateParams.credential_id, - relatedSets = {}; + id = $stateParams.credential_id; - generator.inject(form, { mode: 'edit', related: true, scope: $scope }); - generator.reset(); - $scope.id = id; + init(); - $scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; + function init() { + $scope.id = id; + $scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + $scope.canShareCredential = false; + Wait('start'); + if (!$rootScope.current_user) { + Authorization.restoreUserInfo(); } - }); + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options', + callback: 'choicesReadyCredential' + }); - $scope.canShareCredential = false; + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); - if ($rootScope.current_user.is_superuser) { - $scope.canShareCredential = true; - } else { - Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); - Rest.get() - .success(function(data) { - $scope.canShareCredential = (data.count) ? true : false; - }).error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); - }); + if ($rootScope.current_user && $rootScope.current_user.is_superuser) { + $scope.canShareCredential = true; + } else { + Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); + Rest.get() + .success(function(data) { + $scope.canShareCredential = (data.count) ? true : false; + }).error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); + }); + } + + // if the credential is assigned to an organization, allow permission delegation + // do NOT use $scope.organization in a view directive to determine if a credential is associated with an org + // @todo why not? ^ and what is this type check for a number doing - should this be a type check for undefined? + $scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true; + if ($scope.disablePermissionAssignment) { + $scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.'; + } + setAskCheckboxes(); + KindChange({ + scope: $scope, + form: form, + reset: false + }); + OwnerChange({ scope: $scope }); + $scope.$watch("ssh_key_data", function(val) { + if (val === "" || val === null || val === undefined) { + $scope.keyEntered = false; + $scope.ssh_key_unlock_ask = false; + $scope.ssh_key_unlock = ""; + } else { + $scope.keyEntered = true; + } + }); } function setAskCheckboxes() { @@ -398,64 +356,14 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, } } } - - if ($scope.removeCredentialLoaded) { - $scope.removeCredentialLoaded(); - } - $scope.removeCredentialLoaded = $scope.$on('credentialLoaded', function () { - // if the credential is assigned to an organization, allow permission delegation - // do NOT use $scope.organization in a view directive to determine if a credential is associated with an org - $scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true; - if ($scope.disablePermissionAssignment){ - $scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.'; - } - var set; - for (set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - var orgUrl = ($rootScope.current_user.is_superuser) ? - GetBasePath("organizations") : - $rootScope.current_user.url + "admin_of_organizations?"; - - // create LookUpInit for organizations - LookUpInit({ - scope: $scope, - url: orgUrl, - form: form, - current_item: $scope.organization, - list: OrganizationList, - field: 'organization', - input_type: 'radio', - autopopulateLookup: false - }); - - setAskCheckboxes(); - KindChange({ - scope: $scope, - form: form, - reset: false - }); - OwnerChange({ scope: $scope }); - $scope.$watch("ssh_key_data", function(val) { - if (val === "" || val === null || val === undefined) { - $scope.keyEntered = false; - $scope.ssh_key_unlock_ask = false; - $scope.ssh_key_unlock = ""; - } else { - $scope.keyEntered = true; - } - }); - Wait('stop'); - }); - if ($scope.removeChoicesReady) { $scope.removeChoicesReady(); } - $scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function () { + $scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function() { // Retrieve detail record and prepopulate the form Rest.setUrl(defaultUrl + ':id/'); Rest.get({ params: { id: id } }) - .success(function (data) { + .success(function(data) { if (data && data.summary_fields && data.summary_fields.organization && data.summary_fields.organization.id) { @@ -481,7 +389,6 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; } } - relatedSets = form.relatedSets(data.related); if (!Empty($scope.user)) { $scope.owner = 'user'; @@ -530,118 +437,93 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, }); switch (data.kind) { - case 'aws': - $scope.access_key = data.username; - $scope.secret_key = data.password; - master.access_key = $scope.access_key; - master.secret_key = $scope.secret_key; - break; - case 'ssh': - $scope.ssh_password = data.password; - master.ssh_password = $scope.ssh_password; - break; - case 'rax': - $scope.api_key = data.password; - master.api_key = $scope.api_key; - break; - case 'gce': - $scope.email_address = data.username; - $scope.project = data.project; - break; - case 'azure': - $scope.subscription = data.username; - break; + case 'aws': + $scope.access_key = data.username; + $scope.secret_key = data.password; + master.access_key = $scope.access_key; + master.secret_key = $scope.secret_key; + break; + case 'ssh': + $scope.ssh_password = data.password; + master.ssh_password = $scope.ssh_password; + break; + case 'rax': + $scope.api_key = data.password; + master.api_key = $scope.api_key; + break; + case 'gce': + $scope.email_address = data.username; + $scope.project = data.project; + break; + case 'azure': + $scope.subscription = data.username; + break; } $scope.credential_obj = data; - RelatedSearchInit({ - scope: $scope, - form: form, - relatedSets: relatedSets - }); - RelatedPaginateInit({ - scope: $scope, - relatedSets: relatedSets - }); - $scope.$emit('credentialLoaded'); }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to retrieve Credential: ' + $stateParams.id + '. GET status: ' + status }); + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to retrieve Credential: ' + $stateParams.id + '. GET status: ' + status + }); }); }); - Wait('start'); - - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'kind', - variable: 'credential_kind_options', - callback: 'choicesReadyCredential' - }); - - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'become_method', - variable: 'become_options' - }); - // Save changes to the parent - $scope.formSave = function () { - generator.clearApiErrors(); - generator.checkAutoFill({ scope: $scope }); + $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { FormSave({ scope: $scope, mode: 'edit' }); } }; // Handle Owner change - $scope.ownerChange = function () { + $scope.ownerChange = function() { OwnerChange({ scope: $scope }); }; // Handle Kind change - $scope.kindChange = function () { + $scope.kindChange = function() { KindChange({ scope: $scope, form: form, reset: true }); }; - $scope.formCancel = function () { + $scope.formCancel = function() { $state.transitionTo('credentials'); }; // Related set: Add button - $scope.add = function (set) { + $scope.add = function(set) { $rootScope.flashMessage = null; $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/add'); }; // Related set: Edit button - $scope.edit = function (set, id) { + $scope.edit = function(set, id) { $rootScope.flashMessage = null; $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/' + id); }; // Related set: Delete button - $scope['delete'] = function (set, itm_id, name, title) { + $scope['delete'] = function(set, itm_id, name, title) { $rootScope.flashMessage = null; - var action = function () { + var action = function() { var url = defaultUrl + id + '/' + set + '/'; Rest.setUrl(url); Rest.post({ - id: itm_id, - disassociate: 1 - }) - .success(function () { - $('#prompt-modal').modal('hide'); - $scope.search(form.related[set].iterator); + id: itm_id, + disassociate: 1 }) - .error(function (data, status) { + .success(function() { $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', + // @issue: OLD SEARCH + // $scope.search(form.related[set].iterator); + }) + .error(function(data, status) { + $('#prompt-modal').modal('hide'); + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); }); @@ -657,14 +539,14 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, }; // Password change - $scope.clearPWConfirm = function (fld) { + $scope.clearPWConfirm = function(fld) { // If password value changes, make sure password_confirm must be re-entered $scope[fld] = ''; $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); }; // Respond to 'Ask at runtime?' checkbox - $scope.ask = function (fld, associated) { + $scope.ask = function(fld, associated) { if ($scope[fld + '_ask']) { $scope[fld] = 'ASK'; $("#" + form.name + "_" + fld + "_input").attr("type", "text"); @@ -688,7 +570,7 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, } }; - $scope.clear = function (fld, associated) { + $scope.clear = function(fld, associated) { $scope[fld] = ''; $scope[associated] = ''; $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); @@ -698,9 +580,8 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, } CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', - 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', - 'KindChange', 'OrganizationList', 'LookUpInit', 'Empty', 'OwnerChange', + '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', + 'KindChange', 'Empty', 'OwnerChange', 'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization' ]; diff --git a/awx/ui/client/src/controllers/JobEvents.js b/awx/ui/client/src/controllers/JobEvents.js index bf547dd859..0ea23a76ce 100644 --- a/awx/ui/client/src/controllers/JobEvents.js +++ b/awx/ui/client/src/controllers/JobEvents.js @@ -12,15 +12,17 @@ export function JobEventsList($sce, $filter, $scope, $rootScope, $location, $log, $stateParams, Rest, Alert, JobEventList, GenerateList, - Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, LookUpInit, ToggleChildren, - FormatDate, EventView, Refresh, Wait) { + Prompt, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, LookUpInit, ToggleChildren, + FormatDate, EventView, Wait) { ClearScope(); var list = JobEventList, - defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_events/', //?parent__isnull=1'; - generator = GenerateList, - page; + generator = GenerateList; + + // @issue: OLD SEARCH + // var defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_events/', //?parent__isnull=1'; + // page; list.base = $location.path(); $scope.job_id = $stateParams.id; @@ -191,30 +193,31 @@ export function JobEventsList($sce, $filter, $scope, $rootScope, $location, $log }); }); - SearchInit({ - scope: $scope, - set: 'jobevents', - list: list, - url: defaultUrl - }); - - page = ($stateParams.page) ? parseInt($stateParams.page,10) - 1 : null; - - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl, - page: page - }); - - // Called from Inventories tab, host failed events link: - if ($stateParams.host) { - $scope[list.iterator + 'SearchField'] = 'host'; - $scope[list.iterator + 'SearchValue'] = $stateParams.host; - $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; - } - - $scope.search(list.iterator, $stateParams.page); + // @issue: OLD SEARCH + // SearchInit({ + // scope: $scope, + // set: 'jobevents', + // list: list, + // url: defaultUrl + // }); + // + // page = ($stateParams.page) ? parseInt($stateParams.page,10) - 1 : null; + // + // PaginateInit({ + // scope: $scope, + // list: list, + // url: defaultUrl, + // page: page + // }); + // + // // Called from Inventories tab, host failed events link: + // if ($stateParams.host) { + // $scope[list.iterator + 'SearchField'] = 'host'; + // $scope[list.iterator + 'SearchValue'] = $stateParams.host; + // $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; + // } + // + // $scope.search(list.iterator, $stateParams.page); $scope.toggle = function (id) { ToggleChildren({ @@ -231,21 +234,24 @@ export function JobEventsList($sce, $filter, $scope, $rootScope, $location, $log }; $scope.refresh = function () { - $scope.jobSearchSpin = true; + // @issue: OLD SEARCH + // $scope.jobSearchSpin = true; $scope.jobLoading = true; Wait('start'); - Refresh({ - scope: $scope, - set: 'jobevents', - iterator: 'jobevent', - url: $scope.current_url - }); + + // @issue: OLD SEARCH + // Refresh({ + // scope: $scope, + // set: 'jobevents', + // iterator: 'jobevent', + // url: $scope.current_url + // }); }; } JobEventsList.$inject = ['$sce', '$filter', '$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'JobEventList', - 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'LookUpInit', 'ToggleChildren', 'FormatDate', 'EventView', 'Refresh', 'Wait' + 'generateList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', + 'GetBasePath', 'LookUpInit', 'ToggleChildren', 'FormatDate', 'EventView', 'Wait' ]; export function JobEventsEdit($scope, $rootScope, $compile, $location, $log, $stateParams, JobEventsForm, GenerateForm, diff --git a/awx/ui/client/src/controllers/JobHosts.js b/awx/ui/client/src/controllers/JobHosts.js index cd6c0c65a4..e078bc4df7 100644 --- a/awx/ui/client/src/controllers/JobHosts.js +++ b/awx/ui/client/src/controllers/JobHosts.js @@ -12,13 +12,14 @@ export function JobHostSummaryList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, JobHostList, GenerateList, - Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, Refresh, + Prompt, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, JobStatusToolTip) { ClearScope(); var list = JobHostList, - defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_host_summaries/', + // @issue: OLD SEARCH + // defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_host_summaries/', view = GenerateList, inventory; @@ -58,26 +59,27 @@ export function JobHostSummaryList($scope, $rootScope, $location, $log, $statePa $scope.removeJobReady = $scope.$on('JobReady', function() { view.inject(list, { mode: 'edit', scope: $scope }); - SearchInit({ - scope: $scope, - set: 'jobhosts', - list: list, - url: defaultUrl - }); - - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - - // Called from Inventories tab, host failed events link: - if ($stateParams.host_name) { - $scope[list.iterator + 'SearchField'] = 'host'; - $scope[list.iterator + 'SearchValue'] = $stateParams.host_name; - $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; - } - $scope.search(list.iterator); + // @issue: OLD SEARCH + // SearchInit({ + // scope: $scope, + // set: 'jobhosts', + // list: list, + // url: defaultUrl + // }); + // + // PaginateInit({ + // scope: $scope, + // list: list, + // url: defaultUrl + // }); + // + // // Called from Inventories tab, host failed events link: + // if ($stateParams.host_name) { + // $scope[list.iterator + 'SearchField'] = 'host'; + // $scope[list.iterator + 'SearchValue'] = $stateParams.host_name; + // $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; + // } + // $scope.search(list.iterator); }); Rest.setUrl(GetBasePath('jobs') + $scope.job_id); @@ -107,12 +109,13 @@ export function JobHostSummaryList($scope, $rootScope, $location, $log, $statePa }; $scope.refresh = function () { - $scope.search(list.iterator); + // @issue: OLD SEARCH + // $scope.search(list.iterator); }; } JobHostSummaryList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'JobHostList', - 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'Refresh', 'JobStatusToolTip', 'Wait' + 'generateList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', + 'GetBasePath', 'JobStatusToolTip', 'Wait' ]; diff --git a/awx/ui/client/src/controllers/Jobs.js b/awx/ui/client/src/controllers/Jobs.js index 07e84df3ee..54187896df 100644 --- a/awx/ui/client/src/controllers/Jobs.js +++ b/awx/ui/client/src/controllers/Jobs.js @@ -8,148 +8,105 @@ * @ngdoc function * @name controllers.function:Jobs * @description This controller's for the jobs page -*/ + */ -export function JobsListController ($rootScope, $log, $scope, $compile, $stateParams, - ClearScope, LoadSchedulesScope, - LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait, $state) { + +export function JobsListController($state, $rootScope, $log, $scope, $compile, $stateParams, + ClearScope, Find, DeleteJob, RelaunchJob, AllJobsList, ScheduledJobsList, GetBasePath, Dataset) { ClearScope(); - var jobs_scope, scheduled_scope, - choicesCount = 0, - listCount = 0, - api_complete = false, - scheduledJobsList = _.cloneDeep(ScheduledJobsList); + var list = AllJobsList; - $scope.jobsSelected = true; + init(); - if ($scope.removeListLoaded) { - $scope.removeListLoaded(); + function init() { + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $scope.showJobType = true; + + _.forEach($scope[list.name], buildTooltips); } - $scope.removeListLoaded = $scope.$on('listLoaded', function() { - listCount++; - if (listCount === 2) { - api_complete = true; - } - }); - - - // After all choices are ready, load up the lists and populate the page - if ($scope.removeBuildJobsList) { - $scope.removeBuildJobsList(); + function buildTooltips(job) { + job.status_tip = 'Job ' + job.status + ". Click for details."; } - $scope.removeBuildJobsList = $scope.$on('buildJobsList', function() { - var opt, search_params={}; - if (AllJobsList.fields.type) { - AllJobsList.fields.type.searchOptions = $scope.type_choices; - } - if ($stateParams.status) { - search_params[AllJobsList.iterator + 'SearchField'] = 'status'; - search_params[AllJobsList.iterator + 'SelectShow'] = true; - search_params[AllJobsList.iterator + 'SearchSelectOpts'] = AllJobsList.fields.status.searchOptions; - search_params[AllJobsList.iterator + 'SearchFieldLabel'] = AllJobsList.fields.status.label.replace(//g,' '); - search_params[AllJobsList.iterator + 'SearchType'] = ''; - for (opt in AllJobsList.fields.status.searchOptions) { - if (AllJobsList.fields.status.searchOptions[opt].value === $stateParams.status) { - search_params[AllJobsList.iterator + 'SearchSelectValue'] = AllJobsList.fields.status.searchOptions[opt]; - break; - } - } - } - jobs_scope = $scope.$new(true); + $scope.deleteJob = function(id) { + DeleteJob({ scope: $scope, id: id }); + }; - jobs_scope.viewJob = function (id) { - $state.transitionTo('jobDetail', {id: id}); + $scope.relaunchJob = function(event, id) { + var list, job, typeId; + try { + $(event.target).tooltip('hide'); + } catch (e) { + //ignore + } + + job = Find({ list: list, key: 'id', val: id }); + if (job.type === 'inventory_update') { + typeId = job.inventory_source; + } else if (job.type === 'project_update') { + typeId = job.project; + } else if (job.type === 'job' || job.type === "system_job" || job.type === 'ad_hoc_command') { + typeId = job.id; + } + RelaunchJob({ scope: $scope, id: typeId, type: job.type, name: job.name }); + }; + + $scope.refreshJobs = function() { + $state.go('.', null, { reload: true }); + }; + + $scope.viewJobDetails = function(job) { + + var goToJobDetails = function(state) { + $state.go(state, { id: job.id }, { reload: true }); }; - - jobs_scope.showJobType = true; - LoadJobsScope({ - parent_scope: $scope, - scope: jobs_scope, - list: AllJobsList, - id: 'active-jobs', - url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled,new&order_by=-finished', - pageSize: 20, - searchParams: search_params, - spinner: false - }); - - - scheduled_scope = $scope.$new(true); - scheduledJobsList.basePath = GetBasePath('schedules') + '?next_run__isnull=false'; - LoadSchedulesScope({ - parent_scope: $scope, - scope: scheduled_scope, - list: scheduledJobsList, - pageSize: 20, - id: 'scheduled-jobs-tab', - searchSize: 'col-lg-4 col-md-4 col-sm-4 col-xs-12', - url: scheduledJobsList.basePath - }); - - $scope.refreshJobs = function() { - jobs_scope.search('all_job'); - scheduled_scope.search('schedule'); - }; - - function clearTabs() { - $scope.jobsSelected = false; - $scope.schedulesSelected = false; - } - - $scope.toggleTab = function(tab) { - clearTabs(); - if (tab === "jobs") { - $scope.jobsSelected = true; - } else if (tab === "scheduled") { - $scope.schedulesSelected = true; - } - }; - - $scope.$on('ws-jobs', function() { - $scope.refreshJobs(); - }); - - $scope.$on('ws-schedules', function() { - if (api_complete) { - scheduled_scope.search('schedule'); - } - }); - }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - choicesCount++; - if (choicesCount === 2) { - $scope.$emit('buildJobsList'); + switch (job.type) { + case 'job': + goToJobDetails('jobDetail'); + break; + case 'ad_hoc_command': + goToJobDetails('adHocJobStdout'); + break; + case 'system_job': + goToJobDetails('managementJobStdout'); + break; + case 'project_update': + goToJobDetails('scmUpdateStdout'); + break; + case 'inventory_update': + goToJobDetails('inventorySyncStdout'); + break; } + + }; + + $scope.refreshJobs = function() { + $state.reload(); + }; + + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobs', function() { + $scope.refreshJobs(); }); - Wait('start'); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'status', - variable: 'status_choices', - callback: 'choicesReady' - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'type', - variable: 'type_choices', - callback: 'choicesReady' + if ($rootScope.removeScheduleStatusChange) { + $rootScope.removeScheduleStatusChange(); + } + $rootScope.removeScheduleStatusChange = $rootScope.$on('ScheduleStatusChange', function() { + $state.reload(); }); } -JobsListController.$inject = ['$rootScope', '$log', '$scope', '$compile', '$stateParams', -'ClearScope', 'LoadSchedulesScope', 'LoadJobsScope', -'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', '$state']; +JobsListController.$inject = ['$state', '$rootScope', '$log', '$scope', '$compile', '$stateParams', + 'ClearScope', 'Find', 'DeleteJob', 'RelaunchJob', 'AllJobsList', 'ScheduledJobsList', 'GetBasePath', 'Dataset' +]; diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 832e1bd2e3..380f472a97 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -8,85 +8,64 @@ * @ngdoc function * @name controllers.function:Projects * @description This controller's for the projects page -*/ + */ -export function ProjectsList ($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, rbacUiControlService, - i18n) { - ClearScope(); - - $scope.canAdd = false; - - rbacUiControlService.canAdd('projects') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - Wait('start'); +export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, + Rest, Alert, ProjectList, Prompt, ReturnToCaller, ClearScope, ProcessErrors, + GetBasePath, ProjectUpdate, Wait, GetChoices, Empty, Find, GetProjectIcon, + GetProjectToolTip, $filter, $state, rbacUiControlService, Dataset, i18n) { var list = ProjectList, - defaultUrl = GetBasePath('projects') + ($stateParams.status ? '?status__in=' + $stateParams.status : ''), - view = GenerateList, - base = $location.path().replace(/^\//, '').split('/')[0], - mode = (base === 'projects') ? 'edit' : 'select', - url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl, - choiceCount = 0; - view.inject(list, { mode: mode, scope: $scope }); + defaultUrl = GetBasePath('projects'); - $rootScope.flashMessage = null; - $scope.projectLoading = true; + init(); - if (mode === 'select') { - SelectionInit({ - scope: $scope, - list: list, - url: url, - returnToCaller: 1 - }); - } + function init() { + $scope.canAdd = false; - 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 = i18n._("Start an SCM update"); - $scope.projects[i].scm_schedule_tooltip = i18n._("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 = i18n._('Canceled. Click for details'); - } - - if (project.status === 'running' || project.status === 'updating') { - $scope.projects[i].scm_update_tooltip = i18n._("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 = i18n._('Manual projects do not require an SCM update'); - $scope.projects[i].scm_schedule_tooltip = i18n._('Manual projects do not require a schedule'); - $scope.projects[i].scm_type_class = 'btn-disabled'; - $scope.projects[i].statusTip = i18n._('Not configured for SCM'); - $scope.projects[i].statusIcon = 'none'; - } - } - }); + rbacUiControlService.canAdd('projects') + .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; + + _.forEach($scope[list.name], buildTooltips); + $rootScope.flashMessage = null; + } + + $scope.$watch(`${list.name}`, function() { + _.forEach($scope[list.name], buildTooltips); }); + function buildTooltips(project) { + project.statusIcon = GetProjectIcon(project.status); + project.statusTip = GetProjectToolTip(project.status); + project.scm_update_tooltip = "Start an SCM update"; + project.scm_schedule_tooltip = i18n._("Schedule future SCM updates"); + project.scm_type_class = ""; + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + project.statusTip = i18n._('Canceled. Click for details'); + } + + if (project.status === 'running' || project.status === 'updating') { + project.scm_update_tooltip = i18n._("SCM update currently running"); + project.scm_type_class = "btn-disabled"; + } + if (project.scm_type === 'manual') { + project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); + project.scm_schedule_tooltip = i18n._('Manual projects do not require a schedule'); + project.scm_type_class = 'btn-disabled'; + project.statusTip = i18n._('Not configured for SCM'); + project.statusIcon = 'none'; + } + } + $scope.$on(`ws-jobs`, function(e, data) { var project; $log.debug(data); @@ -98,9 +77,9 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, $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 { + // @issue: OLD SEARCH + // $scope.search(list.iterator, null, null, null, null, false); + } else { project.scm_update_tooltip = "SCM update currently running"; project.scm_type_class = "btn-disabled"; } @@ -111,95 +90,12 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, } }); - 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 - defaultUrl += '?status=' + $stateParams.status; - } - - SearchInit({ - scope: $scope, - set: 'projects', - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - - 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: defaultUrl, - field: 'status', - variable: 'project_status_options', - callback: 'choicesReadyProjectList' - }); - - // Load the list of options for Kind - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'scm_type', - variable: 'project_scm_type_options', - callback: 'choicesReadyProjectList' - }); - - $scope.addProject = function () { - $state.transitionTo('projects.add'); + $scope.addProject = function() { + $state.go('projects.add'); }; - $scope.editProject = function (id) { - $state.transitionTo('projects.edit', {id: id}); + $scope.editProject = function(id) { + $state.go('projects.edit', { project_id: id }); }; if ($scope.removeGoToJobDetails) { @@ -213,7 +109,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, // 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}); + $state.go('scmUpdateStdout', { id: id }); } else { Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + @@ -221,7 +117,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, } }); - $scope.showSCMStatus = function (id) { + $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') { @@ -241,18 +137,19 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, } }; - $scope.deleteProject = function (id, name) { - var action = function () { + $scope.deleteProject = function(id, name) { + var action = function() { $('#prompt-modal').modal('hide'); Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() - .success(function () { - if (parseInt($state.params.id) === id) { - $state.go("^", null, {reload: true}); + .success(function() { + if (parseInt($state.params.project_id) === id) { + $state.go("^", null, { reload: true }); } else { - $scope.search(list.iterator); + // @issue: OLD SEARCH + // $scope.search(list.iterator); } }) .error(function (data, status) { @@ -272,7 +169,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, if ($scope.removeCancelUpdate) { $scope.removeCancelUpdate(); } - $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function (e, url) { + $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function(e, url) { // Cancel the project update process Rest.setUrl(url); Rest.post() @@ -288,12 +185,12 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, if ($scope.removeCheckCancel) { $scope.removeCheckCancel(); } - $scope.removeCheckCancel = $scope.$on('Check_Cancel', function (e, data) { + $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) { + .success(function(data) { if (data.can_cancel) { $scope.$emit('Cancel_Update', url); } else { @@ -306,14 +203,14 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, }); }); - $scope.cancelUpdate = function (id, name) { + $scope.cancelUpdate = function(id, name) { Rest.setUrl(GetBasePath("projects") + id); Rest.get() - .success(function (data) { + .success(function(data) { if (data.related.current_update) { Rest.setUrl(data.related.current_update); Rest.get() - .success(function (data) { + .success(function(data) { $scope.$emit('Check_Cancel', data); }) .error(function (data, status) { @@ -331,15 +228,10 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, }); }; - $scope.refresh = function () { - $scope.search(list.iterator); - }; - - $scope.SCMUpdate = function (project_id, event) { + $scope.SCMUpdate = function(project_id, event) { try { $(event.target).tooltip('hide'); - } - catch(e) { + } catch (e) { // ignore } $scope.projects.every(function(project) { @@ -362,62 +254,52 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, $scope.editSchedules = function(id) { var project = Find({ list: $scope.projects, key: 'id', val: id }); if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) { - $state.go('projectSchedules', {id: id}); + $state.go('projectSchedules', { id: id }); } }; } -ProjectsList.$inject = ['$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', 'rbacUiControlService', - 'i18n' +ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', + 'Rest', 'Alert', 'ProjectList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', + 'GetBasePath', 'ProjectUpdate', 'Wait', 'GetChoices', 'Empty', 'Find', 'GetProjectIcon', + 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService', 'Dataset', 'i18n' ]; +export function ProjectsAdd($scope, $rootScope, $compile, $location, $log, + $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, + GetBasePath, GetProjectPath, GetChoices, Wait, $state, CreateSelect2) { -export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $log, - $stateParams, ProjectsForm, GenerateForm, Rest, Alert, ProcessErrors, - ClearScope, GetBasePath, ReturnToCaller, GetProjectPath, LookUpInit, - OrganizationList, CredentialList, GetChoices, DebugForm, Wait, $state, - CreateSelect2, i18n) { - - Rest.setUrl(GetBasePath('projects')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a project.', 'alert-info'); - } - }); - - ClearScope(); - - // Inject dynamic view var form = ProjectsForm(), - generator = GenerateForm, base = $location.path().replace(/^\//, '').split('/')[0], defaultUrl = GetBasePath('projects'), master = {}; - // remove "type" field from search options - CredentialList = _.cloneDeep(CredentialList); - CredentialList.fields.kind.noSearch = true; + init(); - generator.inject(form, { mode: 'add', related: false, scope: $scope }); - generator.reset(); + function init() { + Rest.setUrl(GetBasePath('projects')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a project.', 'alert-info'); + } + }); + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + } GetProjectPath({ scope: $scope, master: master }); if ($scope.removeChoicesReady) { $scope.removeChoicesReady(); } - $scope.removeChoicesReady = $scope.$on('choicesReady', function () { + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { var i; for (i = 0; i < $scope.scm_type_options.length; i++) { if ($scope.scm_type_options[i].value === '') { - $scope.scm_type_options[i].value="manual"; + $scope.scm_type_options[i].value = "manual"; //$scope.scm_type = $scope.scm_type_options[i]; break; } @@ -440,33 +322,14 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l variable: 'scm_type_options', callback: 'choicesReady' }); - - LookUpInit({ - scope: $scope, - form: form, - list: OrganizationList, - field: 'organization', - input_type: 'radio' - }); - - LookUpInit({ - scope: $scope, - url: GetBasePath('credentials') + '?kind=scm', - form: form, - list: CredentialList, - field: 'credential', - input_type: "radio" - }); - CreateSelect2({ element: '#local-path-select', multiple: false }); // Save - $scope.formSave = function () { - var i, fld, url, data={}; - generator.clearApiErrors(); + $scope.formSave = function() { + var i, fld, url, data = {}; data = {}; for (fld in form.fields) { if (form.fields[fld].type === 'checkbox_group') { @@ -480,8 +343,8 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l } } - if($scope.scm_type.value === "manual"){ - data.scm_type = "" ; + if ($scope.scm_type.value === "manual") { + data.scm_type = ""; data.local_path = $scope.local_path.value; } else { data.scm_type = $scope.scm_type.value; @@ -492,26 +355,18 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l Wait('start'); Rest.setUrl(url); Rest.post(data) - .success(function (data) { + .success(function(data) { $scope.addedItem = data.id; - - Refresh({ - scope: $scope, - set: 'projects', - iterator: 'project', - url: $scope.current_url - }); - - $state.go('projects.edit', {id: data.id}, {reload: true}); + $state.go('projects.edit', { id: data.id }, { reload: true }); }) - .error(function (data, status) { + .error(function(data, status) { Wait('stop'); ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n._('Failed to create new project. POST returned status: ') + status }); }); }; - $scope.scmChange = function () { + $scope.scmChange = function() { // When an scm_type is set, path is not required if ($scope.scm_type) { $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; @@ -520,7 +375,7 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l } // Dynamically update popover values - if($scope.scm_type.value) { + if ($scope.scm_type.value) { switch ($scope.scm_type.value) { case 'git': $scope.urlPopover = i18n._('

Example URLs for GIT SCM include:

  • https://github.com/ansible/ansible.git
  • ' + @@ -528,7 +383,7 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l '

    Note: When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' + 'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' + 'SSH. GIT read only protocol (git://) does not use username or password information.'); - break; + break; case 'svn': $scope.urlPopover = i18n._('

    Example URLs for Subversion SCM include:

    ' + '
    • https://github.com/ansible/ansible
    • svn://servername.example.com/path
    • ' + @@ -548,89 +403,47 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l } }; - - $scope.formCancel = function () { - $state.transitionTo('projects'); + $scope.formCancel = function() { + $state.go('projects'); }; } -ProjectsAdd.$inject = ['Refresh', '$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'ProjectsForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ReturnToCaller', - 'GetProjectPath', 'LookUpInit', 'OrganizationList', 'CredentialList', - 'GetChoices', 'DebugForm', 'Wait', '$state', 'CreateSelect2', 'i18n' -]; +ProjectsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', + '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', + 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n']; export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, - $stateParams, ProjectsForm, GenerateForm, Rest, Alert, ProcessErrors, - RelatedSearchInit, RelatedPaginateInit, Prompt, ClearScope, GetBasePath, - ReturnToCaller, GetProjectPath, Authorization, CredentialList, LookUpInit, - GetChoices, Empty, DebugForm, Wait, SchedulesControllerInit, - SchedulesListInit, SchedulesList, ProjectUpdate, $state, CreateSelect2, - OrganizationList, NotificationsListInit, ToggleNotification, i18n) { + $stateParams, ProjectsForm, Rest, Alert, ProcessErrors, + Prompt, ClearScope, GetBasePath, GetProjectPath, Authorization, + GetChoices, Empty, DebugForm, Wait, ProjectUpdate, $state, CreateSelect2, ToggleNotification, i18n) { ClearScope('htmlTemplate'); + var form = ProjectsForm(), + defaultUrl = GetBasePath('projects') + $stateParams.project_id + '/', + master = {}, + id = $stateParams.project_id; + + init(); + + function init() { + $scope.project_local_paths = []; + $scope.base_dir = ''; + } + $scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) { if (val === false) { $scope.canAdd = false; } }); - // Inject dynamic view - var form = ProjectsForm(), - generator = GenerateForm, - defaultUrl = GetBasePath('projects') + $stateParams.id + '/', - base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, i, - id = $stateParams.id, - relatedSets = {}; - - // remove "type" field from search options - CredentialList = _.cloneDeep(CredentialList); - CredentialList.fields.kind.noSearch = true; - - - SchedulesList.well = false; - generator.inject(form, { - mode: 'edit', - related: true, - scope: $scope - }); - generator.reset(); - - $scope.project_local_paths = []; - $scope.base_dir = ''; - - if ($scope.removerelatedschedules) { - $scope.removerelatedschedules(); - } - $scope.removerelatedschedules = $scope.$on('relatedschedules', function() { - SchedulesListInit({ - scope: $scope, - list: SchedulesList, - choices: null, - related: true - }); - }); - // After the project is loaded, retrieve each related set if ($scope.projectLoadedRemove) { $scope.projectLoadedRemove(); } - $scope.projectLoadedRemove = $scope.$on('projectLoaded', function () { - var set, opts=[]; - - for (set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - - SchedulesControllerInit({ - scope: $scope, - parent_scope: $scope, - iterator: 'schedule' - }); + $scope.projectLoadedRemove = $scope.$on('projectLoaded', function() { + var opts = []; if (Authorization.getUserInfo('is_superuser') === true) { GetProjectPath({ scope: $scope, master: master }); @@ -644,42 +457,19 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, $scope.base_dir = 'You do not have access to view this property'; } - LookUpInit({ - url: GetBasePath('credentials') + '?kind=scm', - scope: $scope, - form: form, - list: CredentialList, - field: 'credential', - input_type: 'radio' - }); - - LookUpInit({ - scope: $scope, - form: form, - current_item: $scope.organization, - list: OrganizationList, - field: 'organization', - input_type: 'radio' - }); - $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; Wait('stop'); - NotificationsListInit({ - scope: $scope, - url: GetBasePath('projects'), - id: $scope.project_obj.id - }); - $scope.scmChange(); }); if ($scope.removeChoicesReady) { $scope.removeChoicesReady(); } - $scope.removeChoicesReady = $scope.$on('choicesReady', function () { + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + let i; for (i = 0; i < $scope.scm_type_options.length; i++) { if ($scope.scm_type_options[i].value === '') { $scope.scm_type_options[i].value = "manual"; @@ -689,7 +479,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, // Retrieve detail record and prepopulate the form Rest.setUrl(defaultUrl); Rest.get({ params: { id: id } }) - .success(function (data) { + .success(function(data) { var fld, i; for (fld in form.fields) { if (form.fields[fld].type === 'checkbox_group') { @@ -705,15 +495,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } if (form.fields[fld].sourceModel && data.summary_fields && data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + $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); - - data.scm_type = (Empty(data.scm_type)) ? 'manual' : data.scm_type; for (i = 0; i < $scope.scm_type_options.length; i++) { @@ -743,18 +530,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }); $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; - - // 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 - }); - $scope.scm_update_tooltip = "Start an SCM update"; $scope.scm_type_class = ""; if (data.status === 'running' || data.status === 'updating') { @@ -791,14 +566,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, var notifier = this.notification; try { $(event.target).tooltip('hide'); - } - catch(e) { + } catch (e) { // ignore } ToggleNotification({ scope: $scope, - url: $scope.project_url, - id: $scope.project_obj.id, + url: $scope.project_obj.url, notifier: notifier, column: column, callback: 'NotificationRefresh' @@ -806,9 +579,9 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }; // Save changes to the parent - $scope.formSave = function () { + $scope.formSave = function() { var fld, i, params; - generator.clearApiErrors(); + //generator.clearApiErrors(); Wait('start'); $rootScope.flashMessage = null; params = {}; @@ -824,8 +597,8 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } } - if($scope.scm_type.value === "manual"){ - params.scm_type = "" ; + if ($scope.scm_type.value === "manual") { + params.scm_type = ""; params.local_path = $scope.local_path.value; } else { params.scm_type = $scope.scm_type.value; @@ -836,37 +609,26 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, Rest.put(params) .success(function() { Wait('stop'); - $state.go($state.current, {}, {reload: true}); + $state.go($state.current, {}, { reload: true }); }) - .error(function (data, status) { + .error(function(data, status) { ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update project: ' + id + '. PUT status: ' + status }); }); }; - // Related set: Add button - $scope.add = function (set) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.id + '/' + set); - }; - - // Related set: Edit button - $scope.edit = function (set, id) { - $rootScope.flashMessage = null; - $location.path('/' + set + '/' + id); - }; - // Related set: Delete button - $scope['delete'] = function (set, itm_id, name, title) { - var action = function () { + $scope['delete'] = function(set, itm_id, name, title) { + var action = function() { var url = GetBasePath('projects') + id + '/' + set + '/'; $rootScope.flashMessage = null; Rest.setUrl(url); Rest.post({ id: itm_id, disassociate: 1 }) - .success(function () { + .success(function() { $('#prompt-modal').modal('hide'); - $scope.search(form.related[set].iterator); + // @issue: OLD SEARCH + // $scope.search(form.related[set].iterator); }) - .error(function (data, status) { + .error(function(data, status) { $('#prompt-modal').modal('hide'); ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); }); @@ -880,7 +642,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, }); }; - $scope.scmChange = function () { + $scope.scmChange = function() { if ($scope.scm_type) { $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; @@ -888,7 +650,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } // Dynamically update popover values - if($scope.scm_type.value) { + if ($scope.scm_type.value) { switch ($scope.scm_type.value) { case 'git': $scope.urlPopover = i18n._('

      Example URLs for GIT SCM include:

      • https://github.com/ansible/ansible.git
      • ' + @@ -896,7 +658,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, '

        Note: When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' + 'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' + 'SSH. GIT read only protocol (git://) does not use username or password information.'); - break; + break; case 'svn': $scope.urlPopover = i18n._('

        Example URLs for Subversion SCM include:

        ' + '
        • https://github.com/ansible/ansible
        • svn://servername.example.com/path
        • ' + @@ -916,7 +678,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } }; - $scope.SCMUpdate = function () { + $scope.SCMUpdate = function() { if ($scope.project_obj.scm_type === "Manual" || Empty($scope.project_obj.scm_type)) { // ignore } else if ($scope.project_obj.status === 'updating' || $scope.project_obj.status === 'running' || $scope.project_obj.status === 'pending') { @@ -926,17 +688,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } }; - $scope.formCancel = function () { + $scope.formCancel = function() { $state.transitionTo('projects'); }; } ProjectsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'ProjectsForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', - 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'GetProjectPath', - 'Authorization', 'CredentialList', 'LookUpInit', 'GetChoices', 'Empty', - 'DebugForm', 'Wait', 'SchedulesControllerInit', 'SchedulesListInit', - 'SchedulesList', 'ProjectUpdate', '$state', 'CreateSelect2', - 'OrganizationList', 'NotificationsListInit', 'ToggleNotification', 'i18n' -]; + '$stateParams', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'Prompt', + 'ClearScope', 'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty', + 'DebugForm', 'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification', 'i18n']; diff --git a/awx/ui/client/src/controllers/Schedules.js b/awx/ui/client/src/controllers/Schedules.js index 88f4129122..edebf66a84 100644 --- a/awx/ui/client/src/controllers/Schedules.js +++ b/awx/ui/client/src/controllers/Schedules.js @@ -18,33 +18,33 @@ GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices) { var base, id, url, parentObject; - base = $location.path().replace(/^\//, '').split('/')[0]; + // base = $location.path().replace(/^\//, '').split('/')[0]; - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function() { - var list = $scope.schedules; - list.forEach(function(element, idx) { - list[idx].play_tip = (element.enabled) ? 'Schedule is Active. Click to temporarily stop.' : 'Schedule is temporarily stopped. Click to activate.'; - }); - }); + // if ($scope.removePostRefresh) { + // $scope.removePostRefresh(); + // } + // $scope.removePostRefresh = $scope.$on('PostRefresh', function() { + // var list = $scope.schedules; + // list.forEach(function(element, idx) { + // list[idx].play_tip = (element.enabled) ? 'Schedule is Active. Click to temporarily stop.' : 'Schedule is temporarily stopped. Click to activate.'; + // }); + // }); - if ($scope.removeParentLoaded) { - $scope.removeParentLoaded(); - } - $scope.removeParentLoaded = $scope.$on('ParentLoaded', function() { - url += "schedules/"; - SchedulesList.well = true; - LoadSchedulesScope({ - parent_scope: $scope, - scope: $scope, - list: SchedulesList, - id: 'schedule-list-target', - url: url, - pageSize: 20 - }); - }); + // if ($scope.removeParentLoaded) { + // $scope.removeParentLoaded(); + // } + // $scope.removeParentLoaded = $scope.$on('ParentLoaded', function() { + // url += "schedules/"; + // SchedulesList.well = true; + // LoadSchedulesScope({ + // parent_scope: $scope, + // scope: $scope, + // list: SchedulesList, + // id: 'schedule-list-target', + // url: url, + // pageSize: 20 + // }); + // }); if ($scope.removeChoicesReady) { @@ -67,7 +67,8 @@ GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices) { }); $scope.refreshJobs = function() { - $scope.search(SchedulesList.iterator); + // @issue: OLD SEARCH + // $scope.search(SchedulesList.iterator); }; Wait('start'); diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index aded8e5824..b4ae59fb39 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -8,108 +8,62 @@ * @ngdoc function * @name controllers.function:Teams * @description This controller's for teams -*/ + */ +export function TeamsList($scope, $rootScope, $log, $stateParams, + Rest, Alert, TeamList, Prompt, ClearScope, ProcessErrors, + GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset) { -export function TeamsList($scope, $rootScope, $location, $log, $stateParams, - Rest, Alert, TeamList, GenerateList, Prompt, SearchInit, PaginateInit, - ReturnToCaller, ClearScope, ProcessErrors, SetTeamListeners, GetBasePath, - SelectionInit, Wait, $state, Refresh, $filter, rbacUiControlService) { ClearScope(); - $scope.canAdd = false; - - rbacUiControlService.canAdd('teams') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - var list = TeamList, - defaultUrl = GetBasePath('teams'), - generator = GenerateList, - paths = $location.path().replace(/^\//, '').split('/'), - mode = (paths[0] === 'teams') ? 'edit' : 'select', - url; + defaultUrl = GetBasePath('teams'); - var injectForm = function() { - generator.inject(list, { mode: mode, scope: $scope }); - }; + init(); - injectForm(); + function init() { + $scope.canAdd = false; - $scope.$on("RefreshTeamsList", function() { - injectForm(); - Refresh({ - scope: $scope, - set: 'teams', - iterator: 'team', - url: GetBasePath('teams') + "?order_by=name&page_size=" + $scope.team_page_size + rbacUiControlService.canAdd('teams') + .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; + _.forEach($scope[list.name], (team) => { + team.organization_name = team.summary_fields.organization.name; }); - }); - $scope.selected = []; - - url = GetBasePath('base') + $location.path() + '/'; - SelectionInit({ - scope: $scope, - list: list, - url: url, - returnToCaller: 1 - }); - - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); + $scope.selected = []; } - $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: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - $scope.search(list.iterator); - - $scope.addTeam = function () { - $state.transitionTo('teams.add'); + $scope.addTeam = function() { + $state.go('teams.add'); }; - $scope.editTeam = function (id) { - $state.transitionTo('teams.edit', {team_id: id}); + $scope.editTeam = function(id) { + $state.go('teams.edit', { team_id: id }); }; - $scope.deleteTeam = function (id, name) { + $scope.deleteTeam = function(id, name) { - var action = function () { + var action = function() { Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() - .success(function () { + .success(function() { Wait('stop'); $('#prompt-modal').modal('hide'); if (parseInt($state.params.team_id) === id) { - $state.go("^", null, {reload: true}); + $state.go('^', null, { reload: true }); } else { - $scope.search(list.iterator); + $state.go('.', null, { reload: true }); } }) - .error(function (data, status) { + .error(function(data, status) { Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors($scope, data, status, null, { @@ -128,18 +82,15 @@ export function TeamsList($scope, $rootScope, $location, $log, $stateParams, }; } -TeamsList.$inject = ['$scope', '$rootScope', '$location', '$log', - '$stateParams', 'Rest', 'Alert', 'TeamList', 'generateList', 'Prompt', - 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'SetTeamListeners', 'GetBasePath', 'SelectionInit', 'Wait', - '$state', 'Refresh', '$filter', 'rbacUiControlService' + +TeamsList.$inject = ['$scope', '$rootScope', '$log', + '$stateParams', 'Rest', 'Alert', 'TeamList', 'Prompt', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset' ]; -export function TeamsAdd($scope, $rootScope, $compile, $location, $log, - $stateParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, - ReturnToCaller, ClearScope, GenerateList, OrganizationList, SearchInit, - PaginateInit, GetBasePath, LookUpInit, Wait, $state) { +export function TeamsAdd($scope, $rootScope, $stateParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, Wait, $state) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //$scope. @@ -155,176 +106,128 @@ export function TeamsAdd($scope, $rootScope, $compile, $location, $log, // Inject dynamic view var defaultUrl = GetBasePath('teams'), form = TeamForm, - generator = GenerateForm, - scope = generator.inject(form, { mode: 'add', related: false }); + generator = GenerateForm; - $rootScope.flashMessage = null; - generator.reset(); + init(); - LookUpInit({ - scope: $scope, - form: form, - current_item: null, - list: OrganizationList, - field: 'organization', - input_type: 'radio' - }); + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $rootScope.flashMessage = null; + } // Save - $scope.formSave = function () { + $scope.formSave = function() { var fld, data; generator.clearApiErrors(); Wait('start'); Rest.setUrl(defaultUrl); data = {}; for (fld in form.fields) { - data[fld] = scope[fld]; + data[fld] = $scope[fld]; } Rest.post(data) - .success(function (data) { + .success(function(data) { Wait('stop'); $rootScope.flashMessage = "New team successfully created!"; $rootScope.$broadcast("EditIndicatorChange", "users", data.id); - $state.go('teams.edit', {team_id: data.id}, {reload: true}); + $state.go('teams.edit', { team_id: data.id }, { reload: true }); }) - .error(function (data, status) { + .error(function(data, status) { Wait('stop'); - ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new team. Post returned status: ' + - status }); + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new team. Post returned status: ' + + status + }); }); }; - $scope.formCancel = function () { - $state.transitionTo('teams'); + $scope.formCancel = function() { + $state.go('teams'); }; } -TeamsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'TeamForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList', - 'OrganizationList', 'SearchInit', 'PaginateInit', 'GetBasePath', - 'LookUpInit', 'Wait', '$state' +TeamsAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state' ]; -export function TeamsEdit($scope, $rootScope, $location, - $stateParams, TeamForm, GenerateForm, Rest, ProcessErrors, - RelatedSearchInit, RelatedPaginateInit, ClearScope, - LookUpInit, GetBasePath, OrganizationList, Wait, $state) { +export function TeamsEdit($scope, $rootScope, $stateParams, + TeamForm, Rest, ProcessErrors, ClearScope, GetBasePath, Wait, $state) { ClearScope(); - var defaultUrl = GetBasePath('teams'), - generator = GenerateForm, - form = TeamForm, + var form = TeamForm, id = $stateParams.team_id, - relatedSets = {}, - set; + defaultUrl = GetBasePath('teams') + id; - $scope.team_id = id; + init(); - $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); + function init() { + $scope.team_id = id; + Rest.setUrl(defaultUrl); + Wait('start'); + Rest.get(defaultUrl).success(function(data) { + setScopeFields(data); + $scope.organization_name = data.summary_fields.organization.name; + + $scope.team_obj = data; + }); + + $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); - generator.inject(form, { mode: 'edit', related: true, scope: $scope }); - generator.reset(); + } - var setScopeFields = function(data){ + // @issue I think all this really want to do is _.forEach(form.fields, (field) =>{ $scope[field] = data[field]}) + function setScopeFields(data) { _(data) - .pick(function(value, key){ - return form.fields.hasOwnProperty(key) === true; - }) - .forEach(function(value, key){ - $scope[key] = value; - }) - .value(); + .pick(function(value, key) { + return form.fields.hasOwnProperty(key) === true; + }) + .forEach(function(value, key) { + $scope[key] = value; + }) + .value(); return; - }; - var setScopeRelated = function(data, related){ - _(related) - .pick(function(value, key){ - return data.related.hasOwnProperty(key) === true; - }) - .forEach(function(value, key){ - relatedSets[key] = { - url: data.related[key], - iterator: value.iterator - }; - }) - .value(); - }; + } // prepares a data payload for a PUT request to the API - var processNewData = function(fields){ + function processNewData(fields) { var data = {}; - _.forEach(fields, function(value, key){ - if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined){ - data[key] = $scope[key]; + _.forEach(fields, function(value, key) { + if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { + data[key] = $scope[key]; } }); return data; + } + + $scope.formCancel = function() { + $state.go('teams', null, { reload: true }); }; - var init = function(){ - var url = defaultUrl + id; - Rest.setUrl(url); - Wait('start'); - Rest.get(url).success(function(data){ - setScopeFields(data); - setScopeRelated(data, form.related); - $scope.organization_name = data.summary_fields.organization.name; - - RelatedSearchInit({ - scope: $scope, - form: form, - relatedSets: relatedSets - }); - - RelatedPaginateInit({ - scope: $scope, - relatedSets: relatedSets - }); - - for (set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - - $scope.team_obj = data; - - LookUpInit({ - url: GetBasePath('organizations'), - scope: $scope, - form: form, - current_item: $scope.organization, - list: OrganizationList, - field: 'organization', - input_type: 'radio' - }); - }); - }; - - $scope.formCancel = function(){ - $state.go('teams', null, {reload: true}); - }; - - $scope.formSave = function(){ - generator.clearApiErrors(); - generator.checkAutoFill(); + $scope.formSave = function() { $rootScope.flashMessage = null; - if ($scope[form.name + '_form'].$valid){ + if ($scope[form.name + '_form'].$valid) { Rest.setUrl(defaultUrl + id + '/'); var data = processNewData(form.fields); - Rest.put(data).success(function(){ - $state.go($state.current, null, {reload: true}); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status }); - }); + Rest.put(data).success(function() { + $state.go($state.current, null, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to retrieve user: ' + + $stateParams.id + '. GET status: ' + status + }); + }); } }; @@ -337,13 +240,8 @@ export function TeamsEdit($scope, $rootScope, $location, return null; } }; - - /* Related Set implementation TDB */ } -TeamsEdit.$inject = ['$scope', '$rootScope', '$location', - '$stateParams', 'TeamForm', 'GenerateForm', 'Rest', - 'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', - 'ClearScope', 'LookUpInit', 'GetBasePath', - 'OrganizationList', 'Wait', '$state' +TeamsEdit.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state' ]; diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index ee7ed6f431..3a7f1b86a8 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -8,14 +8,14 @@ * @ngdoc function * @name controllers.function:Users * @description This controller's the Users page -*/ + */ -import {N_} from "../i18n"; +import { N_ } from "../i18n"; const user_type_options = [ - {type: 'normal' , label: N_('Normal User') }, - {type: 'system_auditor' , label: N_('System Auditor') }, - {type: 'system_administrator', label: N_('System Administrator') }, + { type: 'normal', label: N_('Normal User') }, + { type: 'system_auditor', label: N_('System Auditor') }, + { type: 'system_administrator', label: N_('System Administrator') }, ]; function user_type_sync($scope) { @@ -25,18 +25,17 @@ function user_type_sync($scope) { switch (type_option.type) { case 'system_administrator': $scope.is_superuser = true; - break; + break; case 'system_auditor': $scope.is_system_auditor = true; - break; + break; } }; } -export function UsersList($scope, $rootScope, $location, $log, $stateParams, - Rest, Alert, UserList, GenerateList, Prompt, SearchInit, PaginateInit, - ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, - Wait, $state, Refresh, $filter, rbacUiControlService, i18n) { +export function UsersList($scope, $rootScope, $stateParams, + Rest, Alert, UserList, Prompt, ClearScope, ProcessErrors, GetBasePath, + Wait, $state, $filter, rbacUiControlService, Dataset, i18n) { for (var i = 0; i < user_type_options.length; i++) { user_type_options[i].label = i18n._(user_type_options[i].label); @@ -44,95 +43,57 @@ export function UsersList($scope, $rootScope, $location, $log, $stateParams, ClearScope(); - $scope.canAdd = false; - - rbacUiControlService.canAdd('users') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - var list = UserList, - defaultUrl = GetBasePath('users'), - generator = GenerateList, - base = $location.path().replace(/^\//, '').split('/')[0], - mode = (base === 'users') ? 'edit' : 'select', - url = (base === 'organizations') ? GetBasePath('organizations') + $stateParams.organization_id + '/users/' : - GetBasePath('teams') + $stateParams.team_id + '/users/'; + defaultUrl = GetBasePath('users'); - var injectForm = function() { - generator.inject(UserList, { mode: mode, scope: $scope }); - }; + init(); - injectForm(); + function init() { + $scope.canAdd = false; - $scope.$on("RefreshUsersList", function() { - injectForm(); - Refresh({ - scope: $scope, - set: 'users', - iterator: 'user', - url: GetBasePath('users') + "?order_by=username&page_size=" + $scope.user_page_size - }); - }); + rbacUiControlService.canAdd('users') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); - $scope.selected = []; + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - if (mode === 'select') { - SelectionInit({ scope: $scope, list: list, url: url, returnToCaller: 1 }); + + $rootScope.flashMessage = null; + $scope.selected = []; } - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - // Cleanup after a delete - Wait('stop'); - $('#prompt-modal').modal('hide'); - }); - - $rootScope.flashMessage = null; - SearchInit({ - scope: $scope, - set: 'users', - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - $scope.search(list.iterator); - - $scope.addUser = function () { - $state.transitionTo('users.add'); + $scope.addUser = function() { + $state.go('users.add'); }; - $scope.editUser = function (id) { - $state.transitionTo('users.edit', {user_id: id}); + $scope.editUser = function(id) { + $state.go('users.edit', { user_id: id }); }; - $scope.deleteUser = function (id, name) { + $scope.deleteUser = function(id, name) { - var action = function () { - //$('#prompt-modal').on('hidden.bs.modal', function () { - // Wait('start'); - //}); + var action = function() { $('#prompt-modal').modal('hide'); Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() - .success(function () { + .success(function() { if (parseInt($state.params.user_id) === id) { - $state.go("^", null, {reload: true}); + $state.go('^', null, { reload: true }); } else { - $scope.search(list.iterator); + $state.go('.', null, { reload: true }); } }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); }); }; @@ -145,91 +106,54 @@ export function UsersList($scope, $rootScope, $location, $log, $stateParams, }; } -UsersList.$inject = ['$scope', '$rootScope', '$location', '$log', - '$stateParams', 'Rest', 'Alert', 'UserList', 'generateList', 'Prompt', - 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'Wait', '$state', - 'Refresh', '$filter', 'rbacUiControlService', 'i18n' +UsersList.$inject = ['$scope', '$rootScope', '$stateParams', + 'Rest', 'Alert', 'UserList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', + 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', 'i18n' ]; - - - -export function UsersAdd($scope, $rootScope, $compile, $location, $log, - $stateParams, UserForm, GenerateForm, Rest, Alert, ProcessErrors, - ReturnToCaller, ClearScope, GetBasePath, LookUpInit, OrganizationList, - ResetForm, Wait, CreateSelect2, $state, i18n) { - - for (var i = 0; i < user_type_options.length; i++) { - user_type_options[i].label = i18n._(user_type_options[i].label); - } - - Rest.setUrl(GetBasePath('users')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a user.', 'alert-info'); - } - }); +export function UsersAdd($scope, $rootScope, $stateParams, UserForm, + GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope, + GetBasePath, ResetForm, Wait, CreateSelect2, $state, i18n) { ClearScope(); - // Inject dynamic view var defaultUrl = GetBasePath('organizations'), - form = UserForm, - generator = GenerateForm; + form = UserForm; - generator.inject(form, { mode: 'add', related: false, scope: $scope }); - ResetForm(); + init(); - $scope.ldap_user = false; - $scope.not_ldap_user = !$scope.ldap_user; - $scope.ldap_dn = null; - $scope.socialAuthUser = false; - $scope.external_account = null; + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); - generator.reset(); + $scope.ldap_user = false; + $scope.not_ldap_user = !$scope.ldap_user; + $scope.ldap_dn = null; + $scope.socialAuthUser = false; + $scope.external_account = null; - $scope.user_type_options = user_type_options; - $scope.user_type = user_type_options[0]; - $scope.$watch('user_type', user_type_sync($scope)); - - CreateSelect2({ - element: '#user_user_type', - multiple: false - }); - - // Configure the lookup dialog. If we're adding a user through the Organizations tab, - // default the Organization value. - LookUpInit({ - scope: $scope, - form: form, - current_item: ($stateParams.organization_id !== undefined) ? $stateParams.organization_id : null, - list: OrganizationList, - field: 'organization', - input_type: 'radio' - }); - - if ($stateParams.organization_id) { - $scope.organization = $stateParams.organization_id; - Rest.setUrl(GetBasePath('organizations') + $stateParams.organization_id + '/'); - Rest.get() - .success(function (data) { - $scope.organization_name = data.name; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to lookup Organization: ' + data.id + '. GET returned status: ' + status }); + Rest.setUrl(GetBasePath('users')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a user.', 'alert-info'); + } }); + + $scope.user_type_options = user_type_options; + $scope.user_type = user_type_options[0]; + $scope.$watch('user_type', user_type_sync($scope)); + CreateSelect2({ + element: '#user_user_type', + multiple: false + }); } // Save - $scope.formSave = function () { + $scope.formSave = function() { var fld, data = {}; - generator.clearApiErrors(); - generator.checkAutoFill(); if ($scope[form.name + '_form'].$valid) { if ($scope.organization !== undefined && $scope.organization !== null && $scope.organization !== '') { Rest.setUrl(defaultUrl + $scope.organization + '/users/'); @@ -244,18 +168,17 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log, data.is_system_auditor = $scope.is_system_auditor; Wait('start'); Rest.post(data) - .success(function (data) { + .success(function(data) { var base = $location.path().replace(/^\//, '').split('/')[0]; if (base === 'users') { $rootScope.flashMessage = 'New user successfully created!'; $rootScope.$broadcast("EditIndicatorChange", "users", data.id); - $state.go('users.edit', {user_id: data.id}, {reload: true}); - } - else { + $state.go('users.edit', { user_id: data.id }, { reload: true }); + } else { ReturnToCaller(1); } }) - .error(function (data, status) { + .error(function(data, status) { ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new user. POST returned status: ' + status }); }); } else { @@ -264,69 +187,102 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log, } }; - $scope.formCancel = function () { - $state.transitionTo('users'); + $scope.formCancel = function() { + $state.go('users'); }; // Password change - $scope.clearPWConfirm = function (fld) { + $scope.clearPWConfirm = function(fld) { // If password value changes, make sure password_confirm must be re-entered $scope[fld] = ''; $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); }; } -UsersAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', - 'LookUpInit', 'OrganizationList', 'ResetForm', 'Wait', 'CreateSelect2', '$state', - 'i18n' +UsersAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'UserForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', + 'ResetForm', 'Wait', 'CreateSelect2', '$state', 'i18n' ]; - export function UsersEdit($scope, $rootScope, $location, - $stateParams, UserForm, GenerateForm, Rest, ProcessErrors, - RelatedSearchInit, RelatedPaginateInit, ClearScope, - GetBasePath, ResetForm, Wait, CreateSelect2 ,$state, i18n) { + $stateParams, UserForm, Rest, ProcessErrors, + ClearScope, GetBasePath, ResetForm, Wait, CreateSelect2, $state, i18n) { for (var i = 0; i < user_type_options.length; i++) { user_type_options[i].label = i18n._(user_type_options[i].label); } - ClearScope(); - var defaultUrl = GetBasePath('users'), - generator = GenerateForm, - form = UserForm, + var form = UserForm, master = {}, id = $stateParams.user_id, - relatedSets = {}, - set; + defaultUrl = GetBasePath('users') + id; - generator.inject(form, { mode: 'edit', related: true, scope: $scope }); - generator.reset(); + init(); - $scope.user_type_options = user_type_options; - $scope.user_type = user_type_options[0]; - $scope.$watch('user_type', user_type_sync($scope)); + function init() { + $scope.user_type_options = user_type_options; + $scope.user_type = user_type_options[0]; + $scope.$watch('user_type', user_type_sync($scope)); + Rest.setUrl(defaultUrl); + Wait('start'); + Rest.get(defaultUrl).success(function(data) { + $scope.user_id = id; + $scope.ldap_user = (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') ? true : false; + $scope.not_ldap_user = !$scope.ldap_user; + master.ldap_user = $scope.ldap_user; + $scope.socialAuthUser = (data.auth.length > 0) ? true : false; + $scope.external_account = data.external_account; - $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); + $scope.user_type = $scope.user_type_options[0]; + $scope.is_system_auditor = false; + $scope.is_superuser = false; + if (data.is_system_auditor) { + $scope.user_type = $scope.user_type_options[1]; + $scope.is_system_auditor = true; + } + if (data.is_superuser) { + $scope.user_type = $scope.user_type_options[2]; + $scope.is_superuser = true; + } - var setScopeFields = function(data){ + $scope.user_obj = data; + + CreateSelect2({ + element: '#user_user_type', + multiple: false + }); + + $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + setScopeFields(data); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to retrieve user: ' + + $stateParams.id + '. GET status: ' + status + }); + }); + } + + + function setScopeFields(data) { _(data) - .pick(function(value, key){ - return form.fields.hasOwnProperty(key) === true; - }) - .forEach(function(value, key){ - $scope[key] = value; - }) - .value(); + .pick(function(value, key) { + return form.fields.hasOwnProperty(key) === true; + }) + .forEach(function(value, key) { + $scope[key] = value; + }) + .value(); return; - }; + } $scope.convertApiUrl = function(str) { if (str) { @@ -336,25 +292,12 @@ export function UsersEdit($scope, $rootScope, $location, } }; - var setScopeRelated = function(data, related){ - _(related) - .pick(function(value, key){ - return data.related.hasOwnProperty(key) === true; - }) - .forEach(function(value, key){ - relatedSets[key] = { - url: data.related[key], - iterator: value.iterator - }; - }) - .value(); - }; // prepares a data payload for a PUT request to the API - var processNewData = function(fields){ + var processNewData = function(fields) { var data = {}; - _.forEach(fields, function(value, key){ - if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined){ - data[key] = $scope[key]; + _.forEach(fields, function(value, key) { + if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { + data[key] = $scope[key]; } }); data.is_superuser = $scope.is_superuser; @@ -362,98 +305,37 @@ export function UsersEdit($scope, $rootScope, $location, return data; }; - var init = function(){ - var url = defaultUrl + id; - Rest.setUrl(url); - Wait('start'); - Rest.get(url).success(function(data){ - $scope.user_id = id; - $scope.ldap_user = (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') ? true : false; - $scope.not_ldap_user = !$scope.ldap_user; - master.ldap_user = $scope.ldap_user; - $scope.socialAuthUser = (data.auth.length > 0) ? true : false; - $scope.external_account = data.external_account; - - $scope.user_type = $scope.user_type_options[0]; - $scope.is_system_auditor = false; - $scope.is_superuser = false; - if (data.is_system_auditor) { - $scope.user_type = $scope.user_type_options[1]; - $scope.is_system_auditor = true; - } - if (data.is_superuser) { - $scope.user_type = $scope.user_type_options[2]; - $scope.is_superuser = true; - } - - $scope.user_obj = data; - - CreateSelect2({ - element: '#user_user_type', - multiple: false - }); - - - setScopeFields(data); - setScopeRelated(data, form.related); - - RelatedSearchInit({ - scope: $scope, - form: form, - relatedSets: relatedSets - }); - RelatedPaginateInit({ - scope: $scope, - relatedSets: relatedSets - }); - - for (set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status }); - }); + $scope.formCancel = function() { + $state.go('users', null, { reload: true }); }; - $scope.formCancel = function(){ - $state.go('users', null, {reload: true}); - }; - - $scope.formSave = function(){ - generator.clearApiErrors(); - generator.checkAutoFill(); + $scope.formSave = function() { $rootScope.flashMessage = null; - if ($scope[form.name + '_form'].$valid){ + if ($scope[form.name + '_form'].$valid) { Rest.setUrl(defaultUrl + id + '/'); var data = processNewData(form.fields); - Rest.put(data).success(function(){ - $state.go($state.current, null, {reload: true}); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status }); - }); + Rest.put(data).success(function() { + $state.go($state.current, null, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to retrieve user: ' + + $stateParams.id + '. GET status: ' + status + }); + }); } }; - $scope.clearPWConfirm = function (fld) { + $scope.clearPWConfirm = function(fld) { // If password value changes, make sure password_confirm must be re-entered $scope[fld] = ''; $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); $rootScope.flashMessage = null; }; - - init(); - - /* Related Set implementation TDB */ } UsersEdit.$inject = ['$scope', '$rootScope', '$location', - '$stateParams', 'UserForm', 'GenerateForm', 'Rest', 'ProcessErrors', - 'RelatedSearchInit', 'RelatedPaginateInit', 'ClearScope', 'GetBasePath', + '$stateParams', 'UserForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ResetForm', 'Wait', 'CreateSelect2', '$state', 'i18n' ]; diff --git a/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js b/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js index 246ccb3a68..8d26b9d1c6 100644 --- a/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js +++ b/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js @@ -39,7 +39,7 @@ export default label: i18n._("Hosts") }, { - url: "/#/home/hosts?active-failures=true", + url: "/#/home/hosts?host_search=has_active_failures:true", number: scope.data.hosts.failed, label: i18n._("Failed Hosts"), isFailureCount: true @@ -50,7 +50,7 @@ export default label: i18n._("Inventories"), }, { - url: "/#/inventories?status=sync-failed", + url: "/#/inventories?inventory_search=inventory_sources_with_failures__gt:0", number: scope.data.inventories.inventory_failed, label: i18n._("Inventory Sync Failures"), isFailureCount: true @@ -61,7 +61,7 @@ export default label: i18n._("Projects") }, { - url: "/#/projects?status=failed,canceled", + url: "/#/projects?project_search=status__in:failed,canceled", number: scope.data.projects.failed, label: i18n._("Project Sync Failures"), isFailureCount: true diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.partial.html b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.partial.html deleted file mode 100644 index 5db1583d13..0000000000 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.partial.html +++ /dev/null @@ -1,4 +0,0 @@ -
          -
          -
          -
          diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js index 9009efe609..d07c544942 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js @@ -4,86 +4,52 @@ * All Rights Reserved *************************************************/ -export default - ['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList', - 'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', '$rootScope', 'SearchInit', - function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope, SearchInit){ - var setJobStatus = function(){ - _.forEach($scope.hosts, function(value){ - SetStatus({ - scope: $scope, - host: value - }); - }); - }; - var generator = GenerateList, - list = DashboardHostsList, - defaultUrl = GetBasePath('hosts'); - $scope.hostPageSize = 10; - $scope.editHost = function(id){ - $state.go('dashboardHosts.edit', {id: id}); - }; - $scope.toggleHostEnabled = function(host){ - DashboardHostService.setHostStatus(host, !host.enabled) - .then(function(res){ - var index = _.findIndex($scope.hosts, function(o) {return o.id === res.data.id;}); - $scope.hosts[index].enabled = res.data.enabled; - }); - }; - $scope.$on('PostRefresh', function(){ - $scope.hosts = _.map($scope.hosts, function(value){ - value.inventory_name = value.summary_fields.inventory.name; - value.inventory_id = value.summary_fields.inventory.id; - return value; - }); - setJobStatus(); - }); - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "dashboardHosts.edit") { - $scope.rowBeingEdited = toParams.id; - $scope.listBeingEdited = "hosts"; - } - 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(); - }); - var init = function(){ - $scope.list = list; - $scope.host_active_search = false; - $scope.host_total_rows = hosts.results.length; - $scope.hosts = hosts.results; - setJobStatus(); - generator.inject(list, {mode: 'edit', scope: $scope}); - SearchInit({ - scope: $scope, - set: 'hosts', - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl, - pageSize: 10 - }); - PageRangeSetup({ - scope: $scope, - count: hosts.count, - next: hosts.next, - previous: hosts.previous, - iterator: list.iterator +export default ['$scope', '$state', '$stateParams', 'GetBasePath', 'DashboardHostsList', + 'generateList', 'SetStatus', 'DashboardHostService', '$rootScope', 'Dataset', + function($scope, $state, $stateParams, GetBasePath, DashboardHostsList, + GenerateList, SetStatus, DashboardHostService, $rootScope, Dataset) { + + let list = DashboardHostsList; + init(); + + function init() { + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $scope.$watchCollection(list.name, function() { + $scope[list.name] = _.map($scope.hosts, function(value) { + value.inventory_name = value.summary_fields.inventory.name; + value.inventory_id = value.summary_fields.inventory.id; + return value; + }); + setJobStatus(); }); - $scope.hostLoading = false; - if($state.current.name === "dashboardHosts.edit") { - $scope.rowBeingEdited = $state.params.id; - $scope.listBeingEdited = "hosts"; - } - $scope.search(list.iterator); - }; - init(); - }]; + } + + + function setJobStatus(){ + _.forEach($scope.hosts, function(value) { + SetStatus({ + scope: $scope, + host: value + }); + }); + } + + $scope.editHost = function(id) { + $state.go('dashboardHosts.edit', { id: id }); + }; + + $scope.toggleHostEnabled = function(host) { + DashboardHostService.setHostStatus(host, !host.enabled) + .then(function(res) { + var index = _.findIndex($scope.hosts, function(o) { + return o.id === res.data.id; + }); + $scope.hosts[index].enabled = res.data.enabled; + }); + }; + } +]; diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.partial.html b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.partial.html deleted file mode 100644 index 53511acd75..0000000000 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.partial.html +++ /dev/null @@ -1,4 +0,0 @@ -
          -
          -
          -
          \ No newline at end of file diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js index 50b2e7a2c0..6af1326980 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js @@ -18,7 +18,7 @@ export default function(){ class: 'Form-header-field', ngClick: 'toggleHostEnabled()', type: 'toggle', - editRequired: false, + awToolTip: "

          Indicates if a host is available and should be included in running jobs.

          For hosts that " + "are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.

          ", dataTitle: 'Host Enabled' @@ -28,7 +28,7 @@ export default function(){ name: { label: 'Host Name', type: 'text', - editRequired: true, + value: '{{name}}', awPopOver: "

          Provide a host name, ip address, or ip address:port. Examples include:

          " + "
          myserver.domain.com
          " + @@ -43,12 +43,10 @@ export default function(){ description: { label: 'Description', type: 'text', - editRequired: false }, variables: { label: 'Variables', type: 'textarea', - editRequired: false, rows: 6, class: 'modal-input-xlarge Form-textArea Form-formGroup--fullWidth', dataTitle: 'Host Variables', diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js index eb312af291..0c3c0adb9d 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js @@ -21,11 +21,7 @@ export default [ 'i18n', function(i18n){ basePath: 'unified_jobs', label: '', iconOnly: true, - searchable: false, - searchType: 'select', nosort: true, - searchOptions: [], - searchLabel: 'Job Status', icon: 'icon-job-{{ host.active_failures }}', awToolTip: '{{ host.badgeToolTip }}', awTipPlacement: 'right', @@ -54,24 +50,9 @@ export default [ 'i18n', function(i18n){ columnClass: 'List-staticColumn--toggle', type: 'toggle', ngClick: 'toggleHostEnabled(host)', - searchable: false, nosort: true, awToolTip: "

          Indicates if a host is available and should be included in running jobs.

          For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.

          ", dataTitle: 'Host Enabled', - }, - has_active_failures: { - label: 'Has failed jobs?', - searchSingleValue: true, - searchType: 'boolean', - searchValue: 'true', - searchOnly: true - }, - has_inventory_sources: { - label: 'Has external source?', - searchSingleValue: true, - searchType: 'boolean', - searchValue: 'true', - searchOnly: true } }, diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js deleted file mode 100644 index 4af54c92bb..0000000000 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.route.js +++ /dev/null @@ -1,61 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {templateUrl} from '../../shared/template-url/template-url.factory'; -import listController from './dashboard-hosts-list.controller'; -import editController from './dashboard-hosts-edit.controller'; - -var dashboardHostsList = { - name: 'dashboardHosts', - url: '/home/hosts?:active-failures', - controller: listController, - templateUrl: templateUrl('dashboard/hosts/dashboard-hosts-list'), - data: { - activityStream: true, - activityStreamTarget: 'host' - }, - ncyBreadcrumb: { - parent: 'dashboard', - label: "HOSTS" - }, - resolve: { - hosts: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams){ - var defaultUrl = GetBasePath('hosts') + '?page_size=10' + ($stateParams['active-failures'] ? '&has_active_failures=true' : '' ); - Rest.setUrl(defaultUrl); - return Rest.get().then(function(res){ - var results = _.map(res.data.results, function(value){ - value.inventory_name = value.summary_fields.inventory.name; - value.inventory_id = value.summary_fields.inventory.id; - return value; - }); - res.data.results = results; - return res.data; - }); - }] - } -}; - -var dashboardHostsEdit = { - name: 'dashboardHosts.edit', - url: '/:id', - controller: editController, - templateUrl: templateUrl('dashboard/hosts/dashboard-hosts-edit'), - ncyBreadcrumb: { - parent: 'dashboardHosts', - label: "{{host.name}}" - }, - resolve: { - host: ['$stateParams', 'Rest', 'GetBasePath', function($stateParams, Rest, GetBasePath){ - var defaultUrl = GetBasePath('hosts') + '?id=' + $stateParams.id; - Rest.setUrl(defaultUrl); - return Rest.get().then(function(res){ - return res.data.results[0]; - }); - }] - } -}; - -export {dashboardHostsList, dashboardHostsEdit}; diff --git a/awx/ui/client/src/dashboard/hosts/main.js b/awx/ui/client/src/dashboard/hosts/main.js index 7a02e597d0..935dec2d49 100644 --- a/awx/ui/client/src/dashboard/hosts/main.js +++ b/awx/ui/client/src/dashboard/hosts/main.js @@ -4,17 +4,43 @@ * All Rights Reserved *************************************************/ -import {dashboardHostsList, dashboardHostsEdit} from './dashboard-hosts.route'; import list from './dashboard-hosts.list'; import form from './dashboard-hosts.form'; +import listController from './dashboard-hosts-list.controller'; +import editController from './dashboard-hosts-edit.controller'; import service from './dashboard-hosts.service'; export default - angular.module('dashboardHosts', []) +angular.module('dashboardHosts', []) .service('DashboardHostService', service) .factory('DashboardHostsList', list) .factory('DashboardHostsForm', form) - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(dashboardHostsList); - $stateExtender.addState(dashboardHostsEdit); - }]); + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + + $stateProvider.state({ + name: 'dashboardHosts', + url: '/home/hosts', + lazyLoad: () => stateDefinitions.generateTree({ + url: '/home/hosts', + parent: 'dashboardHosts', + modes: ['edit'], + list: 'DashboardHostsList', + form: 'DashboardHostsForm', + controllers: { + list: listController, + edit: editController + }, + data: { + activityStream: true, + activityStreamTarget: 'host' + }, + ncyBreadcrumb: { + parent: 'dashboard', + label: "HOSTS" + }, + }) + }); + } + ]); diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js index eb4a61b923..8f82f75d55 100644 --- a/awx/ui/client/src/forms/Credentials.js +++ b/awx/ui/client/src/forms/Credentials.js @@ -18,6 +18,8 @@ export default addTitle: i18n._('Create Credential'), //Legend in add mode editTitle: '{{ name }}', //Legend in edit mode name: 'credential', + // the top-most node of generated state tree + stateTree: 'credentials', forceListeners: true, subFormTitles: { credentialSubForm: i18n._('Type Details'), @@ -31,24 +33,22 @@ export default name: { label: i18n._('Name'), type: 'text', - addRequired: true, - editRequired: true, + required: true, autocomplete: false, - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, description: { label: i18n._('Description'), type: 'text', - addRequired: false, - editRequired: false, - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, organization: { - addRequired: false, - editRequired: false, + // interpolated with $rootScope + basePath: "{{$rootScope.current_user.is_superuser ? 'api/v1/organizations' : $rootScope.current_user.url + 'admin_of_organizations'}}", ngShow: 'canShareCredential', label: i18n._('Organization'), type: 'lookup', + list: 'OrganizationList', sourceModel: 'organization', sourceField: 'name', ngClick: 'lookUpOrganization()', @@ -56,7 +56,7 @@ export default dataTitle: i18n._('Organization') + ' ', dataPlacement: 'bottom', dataContainer: "body", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, kind: { label: i18n._('Type'), @@ -64,8 +64,7 @@ export default type: 'select', ngOptions: 'kind.label for kind in credential_kind_options track by kind.value', // select as label for value in array 'kind.label for kind in credential_kind_options', ngChange: 'kindChange()', - addRequired: true, - editRequired: true, + required: true, awPopOver: i18n._('
          \n' + '
          Machine
          \n' + '
          Authentication for remote machine access. This can include SSH keys, usernames, passwords, ' + @@ -88,7 +87,7 @@ export default dataPlacement: 'right', dataContainer: "body", hasSubForm: true, - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, access_key: { label: i18n._('Access Key'), @@ -101,7 +100,7 @@ export default autocomplete: false, apiField: 'username', subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, secret_key: { label: i18n._('Secret Key'), @@ -130,7 +129,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "host": { labelBind: 'hostLabel', @@ -147,7 +146,7 @@ export default init: false }, subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "subscription": { label: i18n._("Subscription ID"), @@ -157,15 +156,15 @@ export default reqExpression: 'subscription_required', init: false }, - addRequired: false, - editRequired: false, + + autocomplete: false, awPopOver: i18n._('

          Subscription ID is an Azure construct, which is mapped to a username.

          '), dataTitle: i18n._('Subscription ID'), dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "username": { labelBind: 'usernameLabel', @@ -178,7 +177,7 @@ export default }, autocomplete: false, subForm: "credentialSubForm", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "email_address": { labelBind: 'usernameLabel', @@ -194,7 +193,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "api_key": { label: i18n._('API Key'), @@ -208,7 +207,7 @@ export default hasShowInputButton: true, clear: false, subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "password": { labelBind: 'passwordLabel', @@ -222,15 +221,13 @@ export default init: false }, subForm: "credentialSubForm", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "ssh_password": { label: i18n._('Password'), type: 'sensitive', ngShow: "kind.value == 'ssh'", ngDisabled: "ssh_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", - addRequired: false, - editRequired: false, subCheckbox: { variable: 'ssh_password_ask', text: i18n._('Ask at runtime?'), @@ -251,8 +248,8 @@ export default }, class: 'Form-textAreaLabel Form-formGroup--fullWidth', elementClass: 'Form-monospace', - addRequired: false, - editRequired: false, + + awDropFile: true, rows: 10, awPopOver: i18n._("SSH key description"), @@ -261,14 +258,12 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: "credentialSubForm", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "ssh_key_unlock": { label: i18n._('Private Key Passphrase'), type: 'sensitive', ngShow: "kind.value == 'ssh' || kind.value == 'scm'", - addRequired: false, - editRequired: false, ngDisabled: "keyEntered === false || ssh_key_unlock_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", subCheckbox: { variable: 'ssh_key_unlock_ask', @@ -293,25 +288,23 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "become_username": { labelBind: 'becomeUsernameLabel', type: 'text', ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ", - addRequired: false, - editRequired: false, + + autocomplete: false, subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "become_password": { labelBind: 'becomePasswordLabel', type: 'sensitive', ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ", ngDisabled: "become_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", - addRequired: false, - editRequired: false, subCheckbox: { variable: 'become_password_ask', text: i18n._('Ask at runtime?'), @@ -326,7 +319,7 @@ export default label: i18n._('Client ID'), subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, secret:{ type: 'sensitive', @@ -335,14 +328,14 @@ export default label: i18n._('Client Secret'), subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, tenant: { type: 'text', label: i18n._('Tenant ID'), subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, authorize: { label: i18n._('Authorize'), @@ -350,7 +343,7 @@ export default ngChange: "toggleCallback('host_config_key')", subForm: 'credentialSubForm', ngShow: "kind.value === 'net'", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, authorize_password: { label: i18n._('Authorize Password'), @@ -359,7 +352,7 @@ export default autocomplete: false, subForm: 'credentialSubForm', ngShow: "authorize && authorize !== 'false'", - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "project": { labelBind: 'projectLabel', @@ -370,14 +363,12 @@ export default dataTitle: i18n._('Project Name'), dataPlacement: 'right', dataContainer: "body", - addRequired: false, - editRequired: false, awRequiredWhen: { reqExpression: 'project_required', init: false }, subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, "domain": { labelBind: 'domainLabel', @@ -391,18 +382,14 @@ export default dataTitle: i18n._('Domain Name'), dataPlacement: 'right', dataContainer: "body", - addRequired: false, - editRequired: false, - subForm: 'credentialSubForm', - ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)', + subForm: 'credentialSubForm' }, "vault_password": { label: i18n._("Vault Password"), type: 'sensitive', ngShow: "kind.value == 'ssh'", ngDisabled: "vault_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", - addRequired: false, - editRequired: false, subCheckbox: { variable: 'vault_password_ask', text: i18n._('Ask at runtime?'), @@ -417,17 +404,17 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '!(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' }, save: { label: 'Save', ngClick: 'formSave()', //$scope.function to call on click, optional ngDisabled: true, - ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' //Disable when $pristine or $invalid, optional + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' //Disable when $pristine or $invalid, optional } }, @@ -437,24 +424,25 @@ export default awToolTip: '{{permissionsTooltip}}', dataTipWatch: 'permissionsTooltip', dataPlacement: 'top', - basePath: 'credentials/:id/access_list/', + basePath: 'api/v1/credentials/{{$stateParams.credential_id}}/access_list/', + search: { + order_by: 'username' + }, type: 'collection', title: i18n._('Permissions'), iterator: 'permission', index: false, open: false, - searchType: 'select', actions: { add: { - ngClick: "addPermission", + ngClick: "$state.go('.add')", label: 'Add', awToolTip: i18n._('Add a permission'), actionClass: 'btn List-buttonSubmit', buttonContent: i18n._('+ ADD'), - ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || !canAdd)' } }, - fields: { username: { key: true, diff --git a/awx/ui/client/src/forms/Groups.js b/awx/ui/client/src/forms/Groups.js index 32723ff5f2..60ad2d39a1 100644 --- a/awx/ui/client/src/forms/Groups.js +++ b/awx/ui/client/src/forms/Groups.js @@ -18,31 +18,31 @@ export default editTitle: '{{ name }}', showTitle: true, name: 'group', + basePath: 'groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventoryManage', + // 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: 'inventoryManage.editGroup', well: false, - fields: { name: { label: 'Name', type: 'text', - addRequired: true, - editRequired: true, - tab: 'properties', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '(!group_obj.summary_fields.user_capabilities.edit || !canAdd)', + required: true, + tab: 'properties' }, description: { label: 'Description', type: 'text', - addRequired: false, - editRequired: false, - tab: 'properties', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '(!group_obj.summary_fields.user_capabilities.edit || !canAdd)', + tab: 'properties' }, variables: { label: 'Variables', type: 'textarea', class: 'Form-textAreaLabel Form-formGroup--fullWidth', - addRequired: false, - editRequird: false, rows: 12, 'default': '---', dataTitle: 'Group Variables', @@ -65,23 +65,23 @@ export default type: 'select', ngOptions: 'source.label for source in source_type_options track by source.value', ngChange: 'sourceChange(source)', - addRequired: false, - editRequired: false, - ngModel: 'source', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '(!group_obj.summary_fields.user_capabilities.edit || !canAdd)', + ngModel: 'source' }, credential: { label: 'Cloud Credential', type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', ngShow: "source && source.value !== '' && source.value !== 'custom'", sourceModel: 'credential', sourceField: 'name', - ngClick: 'lookUpCredential()', + ngClick: 'lookupCredential()', awRequiredWhen: { reqExpression: "cloudCredentialRequired", init: "false" }, - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '(!group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, source_regions: { label: 'Regions', @@ -89,22 +89,20 @@ export default ngOptions: 'source.label for source in source_region_choices track by source.value', multiSelect: true, ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')", - addRequired: false, - editRequired: false, + + dataTitle: 'Source Regions', dataPlacement: 'right', awPopOver: "

          Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + "

          ", dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, instance_filters: { label: 'Instance Filters', type: 'text', ngShow: "source && source.value == 'ec2'", - addRequired: false, - editRequired: false, dataTitle: 'Instance Filters', dataPlacement: 'right', awPopOver: "

          Provide a comma-separated list of filter expressions. " + @@ -118,15 +116,13 @@ export default "

          View the Describe Instances documentation " + "for a complete list of supported filters.

          ", dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, group_by: { label: 'Only Group By', type: 'select', ngShow: "source && source.value == 'ec2'", ngOptions: 'source.label for source in group_by_choices track by source.value', - addRequired: false, - editRequired: false, multiSelect: true, dataTitle: 'Only Group By', dataPlacement: 'right', @@ -144,19 +140,19 @@ export default "
        • Tag None: tags » tag_none
        • " + "

        If blank, all groups above are created except Instance ID.

        ", dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, inventory_script: { label : "Custom Inventory Script", type: 'lookup', + basePath: 'inventory_scripts', + list: 'InventoryScriptList', ngShow: "source && source.value === 'custom'", sourceModel: 'inventory_script', sourceField: 'name', ngClick: 'lookUpInventory_script()' , - addRequired: true, - editRequired: true, ngRequired: "source && source.value === 'custom'", - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)', }, custom_variables: { id: 'custom_variables', @@ -164,8 +160,6 @@ export default ngShow: "source && source.value=='custom' ", type: 'textarea', class: 'Form-textAreaLabel Form-formGroup--fullWidth', - addRequired: false, - editRequired: false, rows: 6, 'default': '---', parseTypeName: 'envParseType', @@ -187,8 +181,6 @@ export default ngShow: "source && source.value == 'ec2'", type: 'textarea', class: 'Form-textAreaLabel Form-formGroup--fullWidth', - addRequired: false, - editRequird: false, rows: 6, 'default': '---', parseTypeName: 'envParseType', @@ -209,12 +201,9 @@ export default vmware_variables: { id: 'vmware_variables', label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'vmware'", type: 'textarea', - addRequired: false, class: 'Form-textAreaLabel Form-formGroup--fullWidth', - editRequird: false, rows: 6, 'default': '---', parseTypeName: 'envParseType', @@ -235,12 +224,9 @@ export default openstack_variables: { id: 'openstack_variables', label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'openstack'", type: 'textarea', - addRequired: false, class: 'Form-textAreaLabel Form-formGroup--fullWidth', - editRequird: false, rows: 6, 'default': '---', parseTypeName: 'envParseType', @@ -263,14 +249,13 @@ export default type: 'checkbox_group', ngShow: "source && (source.value !== '' && source.value !== null)", class: 'Form-checkbox--stacked', - fields: [{ name: 'overwrite', label: 'Overwrite', type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - addRequired: false, - editRequired: false, + + awPopOver: '

        If checked, all child groups and hosts not found on the external source will be deleted from ' + 'the local inventory.

        When not checked, local child hosts and groups not found on the external source will ' + 'remain untouched by the inventory update process.

        ', @@ -278,14 +263,14 @@ export default dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, { name: 'overwrite_vars', label: 'Overwrite Variables', type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - addRequired: false, - editRequired: false, + + awPopOver: '

        If checked, all variables for child groups and hosts will be removed and replaced by those ' + 'found on the external source.

        When not checked, a merge will be performed, combining local variables with ' + 'those found on the external source.

        ', @@ -293,21 +278,19 @@ export default dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, { name: 'update_on_launch', label: 'Update on Launch', type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - addRequired: false, - editRequired: false, awPopOver: '

        Each time a job runs using this inventory, refresh the inventory from the selected source before ' + 'executing job tasks.

        ', dataTitle: 'Update on Launch', dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }] }, update_cache_timeout: { @@ -319,8 +302,6 @@ export default ngShow: "source && source.value !== '' && update_on_launch", spinner: true, "default": 0, - addRequired: false, - editRequired: false, awPopOver: '

        Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + 'and a new inventory sync will be performed.

        ', @@ -333,16 +314,16 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || !canAdd)' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(group_obj.summary_fields.user_capabilities.edit || !canAdd)' } }, diff --git a/awx/ui/client/src/forms/HostGroups.js b/awx/ui/client/src/forms/HostGroups.js index cfa8aea02a..2c920e79f5 100644 --- a/awx/ui/client/src/forms/HostGroups.js +++ b/awx/ui/client/src/forms/HostGroups.js @@ -26,8 +26,7 @@ export default type: 'select', multiple: true, ngOptions: 'group.name for group in inventory_groups track by group.value', - addRequired: true, - editRequired: true, + required: true, awPopOver: "

        Provide a host name, ip address, or ip address:port. Examples include:

        " + "
        myserver.domain.com
        " + "127.0.0.1
        " + diff --git a/awx/ui/client/src/forms/Hosts.js b/awx/ui/client/src/forms/Hosts.js index a1c0e69537..eae5d5c8dc 100644 --- a/awx/ui/client/src/forms/Hosts.js +++ b/awx/ui/client/src/forms/Hosts.js @@ -17,6 +17,7 @@ export default addTitle: 'Create Host', editTitle: '{{ host.name }}', name: 'host', + basePath: 'hosts', well: false, formLabelSize: 'col-lg-3', formFieldSize: 'col-lg-9', @@ -26,7 +27,6 @@ export default class: 'Form-header-field', ngClick: 'toggleHostEnabled(host)', type: 'toggle', - editRequired: false, awToolTip: "

        Indicates if a host is available and should be included in running jobs.

        For hosts that " + "are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.

        ", dataTitle: 'Host Enabled', @@ -36,8 +36,7 @@ export default name: { label: 'Host Name', type: 'text', - addRequired: true, - editRequired: true, + required: true, awPopOver: "

        Provide a host name, ip address, or ip address:port. Examples include:

        " + "
        myserver.domain.com
        " + "127.0.0.1
        " + @@ -47,22 +46,18 @@ export default dataTitle: 'Host Name', dataPlacement: 'right', dataContainer: 'body', - ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || !canAdd)' }, description: { label: 'Description', - type: 'text', - addRequired: false, - editRequired: false, - ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || !canAdd)', + type: 'text' }, variables: { label: 'Variables', type: 'textarea', - addRequired: false, - editRequird: false, rows: 6, - "class": "modal-input-xlarge Form-textArea Form-formGroup--fullWidth", + class: 'Form-formGroup--fullWidth', "default": "---", awPopOver: "

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

        " + "JSON:
        \n" + @@ -85,19 +80,16 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(host.summary_fields.user_capabilities.edit || !canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '!(host.summary_fields.user_capabilities.edit || !canAdd)' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(host.summary_fields.user_capabilities.edit || !canAdd)' } }, - - related: {} - }); diff --git a/awx/ui/client/src/forms/Inventories.js b/awx/ui/client/src/forms/Inventories.js index 0207124476..323ddd1364 100644 --- a/awx/ui/client/src/forms/Inventories.js +++ b/awx/ui/client/src/forms/Inventories.js @@ -4,91 +4,136 @@ * All Rights Reserved *************************************************/ - /** +/** * @ngdoc function * @name forms.function:Inventories * @description This form is for adding/editing an inventory -*/ + */ export default - angular.module('InventoryFormDefinition', ['ScanJobsListDefinition']) - .factory('InventoryFormObject', ['i18n', function(i18n) { +angular.module('InventoryFormDefinition', ['ScanJobsListDefinition']) + .factory('InventoryFormObject', ['i18n', function(i18n) { return { - addTitle: i18n._('New Inventory'), - editTitle: '{{ inventory_name }}', - name: 'inventory', - tabs: true, + addTitle: 'New Inventory', + editTitle: '{{ inventory_name }}', + name: 'inventory', + basePath: 'inventory', + // the top-most node of this generated state tree + stateTree: 'inventories', + tabs: true, - fields: { - inventory_name: { - realName: 'name', - label: i18n._('Name'), - type: 'text', - addRequired: true, - editRequired: true, - capitalize: false, - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + fields: { + inventory_name: { + realName: 'name', + label: i18n._('Name'), + type: 'text', + required: true, + capitalize: false, + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + }, + inventory_description: { + realName: 'description', + label: i18n._('Description'), + type: 'text', + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + }, + organization: { + label: i18n._('Organization'), + type: 'lookup', + basePath: 'organizations', + list: 'OrganizationList', + sourceModel: 'organization', + sourceField: 'name', + awRequiredWhen: { + reqExpression: "organizationrequired", + init: "true" }, - inventory_description: { - realName: 'description', - label: i18n._('Description'), - type: 'text', - addRequired: false, - editRequired: false, - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + class: 'Form-formGroup--fullWidth', + rows: 6, + "default": "---", + awPopOver: "

        Enter inventory 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

        ', + dataTitle: 'Inventory Variables', + dataPlacement: 'right', + dataContainer: 'body', + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' // TODO: get working + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngHide: '(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + } + }, + related: { + permissions: { + awToolTip: i18n._('Please save before assigning permissions'), + dataPlacement: 'top', + basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/access_list/', + type: 'collection', + title: 'Permissions', + iterator: 'permission', + index: false, + open: false, + search: { + order_by: 'username' }, - organization: { - label: i18n._('Organization'), - type: 'lookup', - sourceModel: 'organization', - sourceField: 'name', - ngClick: 'lookUpOrganization()', - awRequiredWhen: { - reqExpression: "organizationrequired", - init: "true" + actions: { + add: { + label: i18n._('Add'), + ngClick: "$state.go('.add')", + awToolTip: 'Add a permission', + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD', + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || !canAdd)' + + } + }, + fields: { + username: { + label: i18n._('User'), + linkBase: 'users', + class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' }, - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - variables: { - label: i18n._('Variables'), - type: 'textarea', - class: 'Form-formGroup--fullWidth', - addRequired: false, - editRequird: false, - rows: 6, - "default": "---", - awPopOver: i18n._("

        Enter inventory 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

        '), - dataTitle: i18n._('Inventory Variables'), - dataPlacement: 'right', - dataContainer: 'body', - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working + role: { + label: i18n._('Role'), + type: 'role', + noSort: true, + class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4', + }, + team_roles: { + label: i18n._('Team Roles'), + type: 'team_roles', + noSort: true, + class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4', + } } - }, + } + }, - buttons: { - cancel: { - ngClick: 'formCancel()', - ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - close: { - ngClick: 'formCancel()', - ngHide: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - save: { - ngClick: 'formSave()', - ngDisabled: true, - ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - } - }, - - related: { + relatedSets: function(urls) { + return { permissions: { awToolTip: i18n._('Please save before assigning permissions'), dataPlacement: 'top', @@ -102,7 +147,7 @@ export default actions: { add: { ngClick: "addPermission", - label: 'Add', + label: i18n._('Add'), awToolTip: i18n._('Add a permission'), actionClass: 'btn List-buttonSubmit', buttonContent: i18n._('+ ADD'), @@ -133,28 +178,22 @@ export default } } } - }, - - relatedSets: function(urls) { - return { - permissions: { - iterator: 'permission', - url: urls.access_list - } - }; - } + }; + } };}]) - .factory('InventoryForm', ['InventoryFormObject', 'ScanJobsList', + + .factory('InventoryForm', ['InventoryFormObject', 'ScanJobsList', function(InventoryFormObject, ScanJobsList) { return function() { var itm; for (itm in InventoryFormObject.related) { if (InventoryFormObject.related[itm].include === "ScanJobsList") { - InventoryFormObject.related[itm] = ScanJobsList; - InventoryFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list + InventoryFormObject.related[itm] = ScanJobsList; + InventoryFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list } } return InventoryFormObject; }; - }]); + } + ]); diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 10d5119407..bc61cf1c7d 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -19,26 +19,27 @@ export default addTitle: i18n._('New Job Template'), editTitle: '{{ name }}', - name: 'job_templates', - base: 'job_templates', + name: 'job_template', + basePath: 'job_templates', + // the top-most node of generated state tree + stateTree: 'jobTemplates', tabs: true, + // (optional) array of supporting templates to ng-include inside generated html + include: ['/static/partials/survey-maker-modal.html'], fields: { name: { label: i18n._('Name'), type: 'text', - addRequired: true, - editRequired: true, - column: 1, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)', + required: true, + column: 1 }, description: { label: i18n._('Description'), type: 'text', - addRequired: false, - editRequired: false, column: 1, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, job_type: { label: i18n._('Job Type'), @@ -46,8 +47,7 @@ export default ngOptions: 'type.label for type in job_type_options track by type.value', ngChange: 'jobTypeChange()', "default": 0, - addRequired: true, - editRequired: true, + required: true, column: 1, awPopOver: i18n._("

        When this template is submitted as a job, setting the type to run will execute the playbook, running tasks " + " on the selected hosts.

        Setting the type to check will not execute the playbook. Instead, ansible will check playbook " + @@ -61,14 +61,15 @@ export default ngShow: "!job_type.value || job_type.value !== 'scan'", text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, inventory: { label: i18n._('Inventory'), type: 'lookup', + basePath: 'inventory', + list: 'InventoryList', sourceModel: 'inventory', sourceField: 'name', - ngClick: 'lookUpInventory()', awRequiredWhen: { reqExpression: '!ask_inventory_on_launch', alwaysShowAsterisk: true @@ -84,7 +85,7 @@ export default ngShow: "!job_type.value || job_type.value !== 'scan'", text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, project: { label: i18n._('Project'), @@ -94,9 +95,10 @@ export default 'class': "{{!(job_type.value === 'scan' && project_name !== 'Default') ? 'hidden' : ''}}", }, type: 'lookup', + list: 'ProjectList', + basePath: 'projects', sourceModel: 'project', sourceField: 'name', - ngClick: 'lookUpProject()', awRequiredWhen: { reqExpression: "projectrequired", init: "true" @@ -106,7 +108,7 @@ export default dataTitle: i18n._('Project'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, playbook: { label: i18n._('Playbook'), @@ -128,9 +130,13 @@ export default credential: { label: i18n._('Machine Credential'), type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + search: { + kind: 'ssh' + }, sourceModel: 'credential', sourceField: 'name', - ngClick: 'lookUpCredential()', awRequiredWhen: { reqExpression: '!ask_credential_on_launch', alwaysShowAsterisk: true @@ -146,38 +152,42 @@ export default variable: 'ask_credential_on_launch', text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, cloud_credential: { label: i18n._('Cloud Credential'), type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + search: { + cloud: 'true' + }, sourceModel: 'cloud_credential', sourceField: 'name', - ngClick: 'lookUpCloudcredential()', - addRequired: false, - editRequired: false, column: 1, awPopOver: i18n._("

        Selecting an optional cloud credential in the job template will pass along the access credentials to the " + "running playbook, allowing provisioning into the cloud without manually passing parameters to the included modules.

        "), dataTitle: i18n._('Cloud Credential'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, network_credential: { label: i18n._('Network Credential'), type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + search: { + kind: 'net' + }, sourceModel: 'network_credential', sourceField: 'name', - ngClick: 'lookUpNetworkcredential()', - addRequired: false, - editRequired: false, column: 1, awPopOver: i18n._("

        Network credentials are used by Ansible networking modules to connect to and manage networking devices.

        "), dataTitle: i18n._('Network Credential'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, forks: { label: i18n._('Forks'), @@ -187,8 +197,6 @@ export default min: 0, spinner: true, "default": '0', - addRequired: false, - editRequired: false, 'class': "input-small", column: 1, awPopOver: i18n._('

        The number of parallel or simultaneous processes to use while executing the playbook. 0 signifies ' + @@ -197,13 +205,11 @@ export default dataTitle: i18n._('Forks'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' // TODO: get working }, limit: { label: i18n._('Limit'), type: 'text', - addRequired: false, - editRequired: false, column: 1, awPopOver: i18n._("

        Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. " + "Multiple patterns can be separated by ; : or ,

        For more information and examples see " + @@ -215,28 +221,25 @@ export default variable: 'ask_limit_on_launch', text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, verbosity: { label: i18n._('Verbosity'), type: 'select', ngOptions: 'v.label for v in verbosity_options track by v.value', "default": 1, - addRequired: true, - editRequired: true, + required: true, column: 1, awPopOver: i18n._("

        Control the level of output ansible will produce as the playbook executes.

        "), dataTitle: i18n._('Verbosity'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, job_tags: { label: i18n._('Job Tags'), type: 'textarea', rows: 5, - addRequired: false, - editRequired: false, 'elementClass': 'Form-textInput', column: 2, awPopOver: i18n._("

        Provide a comma separated list of tags.

        \n" + @@ -249,14 +252,12 @@ export default variable: 'ask_tags_on_launch', text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, skip_tags: { label: i18n._('Skip Tags'), type: 'textarea', rows: 5, - addRequired: false, - editRequired: false, 'elementClass': 'Form-textInput', column: 2, awPopOver: i18n._("

        Provide a comma separated list of tags.

        \n" + @@ -269,7 +270,7 @@ export default variable: 'ask_skip_tags_on_launch', text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, checkbox_group: { label: i18n._('Options'), @@ -278,21 +279,17 @@ export default name: 'become_enabled', label: i18n._('Enable Privilege Escalation'), type: 'checkbox', - addRequired: false, - editRequird: false, column: 2, awPopOver: i18n._("

        If enabled, run this playbook as an administrator. This is the equivalent of passing the --become option to the ansible-playbook command.

        "), dataPlacement: 'right', dataTitle: i18n._('Become Privilege Escalation'), dataContainer: "body", labelClass: 'stack-inline', - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, { name: 'allow_callbacks', label: i18n._('Allow Provisioning Callbacks'), type: 'checkbox', - addRequired: false, - editRequird: false, ngChange: "toggleCallback('host_config_key')", column: 2, awPopOver: i18n._("

        Enables creation of a provisioning callback URL. Using the URL a host can contact Tower and request a configuration update " + @@ -301,14 +298,12 @@ export default dataTitle: i18n._('Allow Provisioning Callbacks'), dataContainer: "body", labelClass: 'stack-inline', - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }] }, callback_url: { label: i18n._('Provisioning Callback URL'), type: 'text', - addRequired: false, - editRequired: false, readonly: true, ngShow: "allow_callbacks && allow_callbacks !== 'false'", column: 2, @@ -317,7 +312,7 @@ export default dataPlacement: 'top', dataTitle: i18n._('Provisioning Callback URL'), dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, host_config_key: { label: i18n._('Host Config Key'), @@ -331,7 +326,7 @@ export default dataPlacement: 'right', dataTitle: i18n._("Host Config Key"), dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, labels: { label: i18n._('Labels'), @@ -339,21 +334,17 @@ export default class: 'Form-formGroup--fullWidth', ngOptions: 'label.label for label in labelOptions track by label.value', multiSelect: true, - addRequired: false, - editRequired: false, dataTitle: i18n._('Labels'), dataPlacement: 'right', awPopOver: i18n._("

        Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.

        "), dataContainer: 'body', - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' }, variables: { label: i18n._('Extra Variables'), type: 'textarea', class: 'Form-textAreaLabel Form-formGroup--fullWidth', rows: 6, - addRequired: false, - editRequired: false, "default": "---", column: 2, awPopOver: i18n._("

        Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter " + @@ -369,14 +360,14 @@ export default variable: 'ask_variables_on_launch', text: i18n._('Prompt on launch') }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || !canAdd)' // TODO: get working } }, buttons: { //for now always generates

- - + + + -
- -
- -
-
- Filter -
- - -
-
- {{ type.label }} -
-
-
-
-
- - -
-
-
-
-
- -
-
- {{tag.label}} : {{ tag.name }} - -
-
-
-
-
- diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js deleted file mode 100644 index 228bcfeac7..0000000000 --- a/awx/ui/client/src/search/tagSearch.service.js +++ /dev/null @@ -1,226 +0,0 @@ -export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', function(Rest, $q, GetBasePath, Wait, ProcessErrors, $log) { - var that = this; - // parse the field config object to return - // one of the searchTypes (for the left dropdown) - this.buildType = function (field, key, id) { - var obj = {}; - // build the value (key) - var value; - if (field.searchField && field.filterBySearchField === true){ - value = field.searchField; - } - else if (field.sourceModel && field.sourceField) { - value = field.sourceModel + '__' + field.sourceField; - obj.related = true; - } else if (typeof(field.key) === String) { - value = field.key; - } else { - value = key; - } - - // build the label - var label = field.searchLabel || field.label; - - // build the search type - var type, typeOptions; - if (field.searchType === 'select') { - type = 'select'; - typeOptions = field.searchOptions || []; - } else if (field.searchType === 'boolean') { - type = 'select'; - typeOptions = field.typeOptions || [{label: "Yes", value: true}, - {label: "No", value: false}]; - } else { - type = 'text'; - } - - if (field.searchDefault) { - obj.default = true; - } - - obj.id = id; - obj.value = value; - obj.label = label; - obj.type = type; - obj.basePath = field.basePath || null; - - // return the built option - if (type === 'select') { - obj.typeOptions = typeOptions; - } - - return obj; - }; - - // given the fields that are searchable, - // return searchTypes in the format the view can use - this.getSearchTypes = function(list, basePath) { - Wait("start"); - var defer = $q.defer(); - - var options = Object - .keys(list) - .filter(function(fieldType) { - return list[fieldType].noSearch !== true; - }) - .map(function(key, id) { - return that.buildType(list[key], key, id); - }); - - var needsRequest, passThrough; - - // splits off options that need a request from - // those that don't - var partitionedOptions = _.partition(options, function(opt) { - return (opt.typeOptions && !opt.typeOptions - .length) ? true : false; - }); - - needsRequest = partitionedOptions[0]; - passThrough = partitionedOptions[1]; - - var joinOptions = function() { - var options = _.sortBy(_ - .flatten([needsRequest, passThrough]), function(opt) { - return opt.id; - }); - - // put default first - return _.flatten(_.partition(options, opt => opt.default)); - }; - - if (needsRequest.length) { - // make the options request to reutrn the typeOptions - var url = needsRequest[0].basePath ? GetBasePath(needsRequest[0].basePath) : basePath; - if(url.indexOf('null') === -1 ){ - Rest.setUrl(url); - Rest.options() - .success(function (data) { - try { - var options = data.actions.GET; - needsRequest = needsRequest - .map(function (option) { - option.typeOptions = options[option - .value] - .choices - .map(function(i) { - return { - value: i[0], - label: i[1] - }; - }); - return option; - }); - } - catch(err){ - if (!basePath){ - $log.error('Cannot retrieve OPTIONS because the basePath parameter is not set on the list with the following fieldset: \n', list); - } - else { $log.error(err); } - } - Wait("stop"); - defer.resolve(joinOptions()); - }) - .error(function (data, status) { - Wait("stop"); - defer.reject("options request failed"); - ProcessErrors(null, data, status, null, { - hdr: 'Error!', - msg: 'Getting type options failed'}); - }); - } - - } else { - Wait("stop"); - defer.resolve(joinOptions()); - } - - return defer.promise; - }; - - // returns the url with filter params - this.updateFilteredUrl = function(basePath, tags, pageSize, searchParams) { - // remove the chain directive from all the urls that might have - // been added previously - tags = (tags || []).map(function(val) { - if (val.url.indexOf("chain__") !== -1) { - val.url = val.url.substring(("chain__").length); - } - return val; - }); - - // separate those tags with the related: true attribute - var separateRelated = _.partition(tags, function(i) { - return i.related; - }); - - var relatedTags = separateRelated[0]; - var nonRelatedTags = separateRelated[1]; - - if (relatedTags.length > 1) { - // separate query params that need the change directive - // but have different keys - var chainGroups = _.groupBy(relatedTags, function(i) { - return i.value; - }); - - // iterate over those groups and add the "chain__" to the - // beginning of all but the first of each url - relatedTags = _.flatten(_.map(chainGroups, function(group) { - return group.map(function(val, i) { - if (i !== 0) { - val.url = "chain__" + val.url; - } - return val; - }); - })); - - // combine the related and non related tags after chainifying - tags = relatedTags.concat(nonRelatedTags); - } - - var returnedUrl = basePath; - returnedUrl += (basePath.indexOf("?") > - 1) ? "&" : "?"; - - return returnedUrl + - (tags || []).map(function (t) { - return t.url; - }).join("&") + "&page_size=" + pageSize + - ((searchParams) ? "&" + searchParams : ""); - }; - - // given the field and input filters, create the tag object - this.getTag = function(field, textVal, selectVal) { - var tag = _.clone(field); - if (tag.type === "text") { - tag.url = tag.value + "__icontains=" + encodeURIComponent(textVal); - tag.name = textVal; - } else if (selectVal.value && typeof selectVal.value === 'string' && selectVal.value.indexOf("=") > 0) { - tag.url = selectVal.value; - tag.name = selectVal.label; - } else { - tag.url = tag.value + "=" + selectVal.value; - tag.name = selectVal.label; - } - return tag; - }; - - // returns true if the newTag is already in the list of tags - this.isDuplicate = function(tags, newTag) { - return (tags - .filter(function(tag) { - return (tag.url === newTag.url); - }).length > 0); - }; - - // returns an array of tags (or empty array if there are none) - // .slice(0) is used so the currentTags variable is not directly mutated - this.getCurrentTags = function(currentTags) { - if (currentTags && currentTags.length) { - return currentTags.slice(0); - } - return []; - }; - - return this; -}]; diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index ab3bdbf8cc..f4517f47cf 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -33,7 +33,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) $rootScope.flashMessage = null; - $('#form-modal .modal-body').empty(); + //$('#form-modal .modal-body').empty(); $('#form-modal2 .modal-body').empty(); $('.tooltip').each(function() { @@ -515,7 +515,9 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) */ .factory('Wait', ['$rootScope', function($rootScope) { + return function(directive) { + /* @todo re-enable var docw, doch, spinnyw, spinnyh; if (directive === 'start' && !$rootScope.waiting) { $rootScope.waiting = true; @@ -536,6 +538,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) $rootScope.waiting = false; }); } + */ }; } ]) diff --git a/awx/ui/client/src/shared/column-sort/column-sort.controller.js b/awx/ui/client/src/shared/column-sort/column-sort.controller.js new file mode 100644 index 0000000000..e5ce39ff3c --- /dev/null +++ b/awx/ui/client/src/shared/column-sort/column-sort.controller.js @@ -0,0 +1,54 @@ +export default ['$scope', '$state', 'QuerySet', 'GetBasePath', + function($scope, $state, qs, GetBasePath) { + + let queryset, path, + order_by = $state.params[`${$scope.columnIterator}_search`].order_by, + activeField = isDescending(order_by) ? order_by.substring(1, order_by.length) : order_by; + + function isDescending(str) { + if (str){ + return str.charAt(0) === '-'; + } + else{ + // default to ascending order if none is supplied + return false; + } + } + function invertOrderBy(str) { + return order_by.charAt(0) === '-' ? `${str.substring(1, str.length)}` : `-${str}`; + } + $scope.orderByIcon = function() { + // column sort is inactive + if (activeField !== $scope.columnField) { + return 'fa-sort'; + } + // column sort is active (governed by order_by) and descending + else if (isDescending(order_by)) { + return 'fa-sort-down'; + } + // column sort is active governed by order_by) and asscending + else { + return 'fa-sort-up'; + } + }; + + $scope.toggleColumnOrderBy = function() { + // toggle active sort order + if (activeField === $scope.columnField) { + order_by = invertOrderBy(order_by); + } + // set new active sort order + else { + order_by = $scope.columnField; + } + queryset = _.merge($state.params[`${$scope.columnIterator}_search`], { order_by: order_by }); + path = GetBasePath($scope.basePath) || $scope.basePath; + $state.go('.', { [$scope.iterator + '_search']: queryset }); + qs.search(path, queryset).then((res) =>{ + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); + }; + + } +]; diff --git a/awx/ui/client/src/shared/column-sort/column-sort.directive.js b/awx/ui/client/src/shared/column-sort/column-sort.directive.js new file mode 100644 index 0000000000..d8e5f54bba --- /dev/null +++ b/awx/ui/client/src/shared/column-sort/column-sort.directive.js @@ -0,0 +1,19 @@ +export default ['templateUrl', function(templateUrl) { + return { + restrict: 'AE', + replace: true, + scope: { + collection: '=', + dataset: '=', + basePath: '@', + columnOrderBy: '@', + columnNoSort: '@', + columnCustomClass: '@', + columnIterator: '@', + columnField: '@', + columnLabel: '@', + }, + controller: 'ColumnSortController', + templateUrl: templateUrl('shared/column-sort/column-sort') + }; +}]; diff --git a/awx/ui/client/src/shared/column-sort/column-sort.partial.html b/awx/ui/client/src/shared/column-sort/column-sort.partial.html new file mode 100644 index 0000000000..96ea648b6c --- /dev/null +++ b/awx/ui/client/src/shared/column-sort/column-sort.partial.html @@ -0,0 +1,4 @@ + + {{columnLabel}} + + diff --git a/awx/ui/client/src/shared/column-sort/main.js b/awx/ui/client/src/shared/column-sort/main.js new file mode 100644 index 0000000000..378a5a1d02 --- /dev/null +++ b/awx/ui/client/src/shared/column-sort/main.js @@ -0,0 +1,7 @@ +import directive from './column-sort.directive'; +import controller from './column-sort.controller'; + +export default + angular.module('ColumnSortModule', []) + .directive('columnSort', directive) + .controller('ColumnSortController', controller); diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index 289e04b58d..54a5eed72b 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -5,7 +5,7 @@ *************************************************/ - /** +/** * @ngdoc function * @name shared.function:directives * @description @@ -16,114 +16,115 @@ export default angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) - // awpassmatch: Add to password_confirm field. Will test if value - // matches that of 'input[name="password"]' - .directive('awpassmatch', function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var associated = attrs.awpassmatch, - password = $('input[name="' + associated + '"]').val(); - if (viewValue === password) { - // it is valid - ctrl.$setValidity('awpassmatch', true); - return viewValue; - } - // Invalid, return undefined (no model update) - ctrl.$setValidity('awpassmatch', false); +// awpassmatch: Add to password_confirm field. Will test if value +// matches that of 'input[name="password"]' +.directive('awpassmatch', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var associated = attrs.awpassmatch, + password = $('input[name="' + associated + '"]').val(); + if (viewValue === password) { + // it is valid + ctrl.$setValidity('awpassmatch', true); return viewValue; - }); - } - }; - }) + } + // Invalid, return undefined (no model update) + ctrl.$setValidity('awpassmatch', false); + return viewValue; + }); + } + }; +}) - // caplitalize Add to any input field where the first letter of each - // word should be capitalized. Use in place of css test-transform. - // For some reason "text-transform: capitalize" in breadcrumbs - // causes a break at each blank space. And of course, - // "autocapitalize='word'" only works in iOS. Use this as a fix. - .directive('capitalize', function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var values = viewValue.split(" "), - result = "", i; - for (i = 0; i < values.length; i++){ - result += values[i].charAt(0).toUpperCase() + values[i].substr(1) + ' '; - } - result = result.trim(); - if (result !== viewValue) { - ctrl.$setViewValue(result); - ctrl.$render(); - } - return result; - }); - } - }; - }) +// caplitalize Add to any input field where the first letter of each +// word should be capitalized. Use in place of css test-transform. +// For some reason "text-transform: capitalize" in breadcrumbs +// causes a break at each blank space. And of course, +// "autocapitalize='word'" only works in iOS. Use this as a fix. +.directive('capitalize', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var values = viewValue.split(" "), + result = "", + i; + for (i = 0; i < values.length; i++) { + result += values[i].charAt(0).toUpperCase() + values[i].substr(1) + ' '; + } + result = result.trim(); + if (result !== viewValue) { + ctrl.$setViewValue(result); + ctrl.$render(); + } + return result; + }); + } + }; +}) - // chkPass - // - // Enables use of js/shared/pwdmeter.js to check strengh of passwords. - // See controllers/Users.js for example. - // - .directive('chkPass', [ function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - $(elm).keyup(function() { - var validity = true; - if (elm.val()) { - if ($AnsibleConfig.password_length) { - validity = (ctrl.$modelValue.length >= $AnsibleConfig.password_length); - ctrl.$setValidity('password_length', validity); - } - if ($AnsibleConfig.password_hasLowercase) { - validity = (/[a-z]/.test(ctrl.$modelValue)); - ctrl.$setValidity('hasLowercase', validity); - } - if ($AnsibleConfig.password_hasUppercase) { - validity = (/[A-Z]/.test(ctrl.$modelValue)); - ctrl.$setValidity('hasUppercase', validity); - } - if ($AnsibleConfig.password_hasNumber) { - validity = (/[0-9]/.test(ctrl.$modelValue)); - ctrl.$setValidity('hasNumber', validity); - } - if ($AnsibleConfig.password_hasSymbol) { - validity = (/[\\#@$-/:-?{-~!"^_`\[\]]/.test(ctrl.$modelValue)); - ctrl.$setValidity('hasSymbol', validity); - } - } else { - validity = true; - if ($AnsibleConfig.password_length) { - ctrl.$setValidity('password_length', validity); - } - if ($AnsibleConfig.password_hasLowercase) { - ctrl.$setValidity('hasLowercase', validity); - } - if ($AnsibleConfig.password_hasUppercase) { - ctrl.$setValidity('hasUppercase', validity); - } - if ($AnsibleConfig.password_hasNumber) { - ctrl.$setValidity('hasNumber', validity); - } - if ($AnsibleConfig.password_hasSymbol) { - ctrl.$setValidity('hasSymbol', validity); - } +// chkPass +// +// Enables use of js/shared/pwdmeter.js to check strengh of passwords. +// See controllers/Users.js for example. +// +.directive('chkPass', [function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + $(elm).keyup(function() { + var validity = true; + if (elm.val()) { + if ($AnsibleConfig.password_length) { + validity = (ctrl.$modelValue.length >= $AnsibleConfig.password_length); + ctrl.$setValidity('password_length', validity); } - if (!scope.$$phase) { - scope.$digest(); + if ($AnsibleConfig.password_hasLowercase) { + validity = (/[a-z]/.test(ctrl.$modelValue)); + ctrl.$setValidity('hasLowercase', validity); } - }); - } - }; - }]) + if ($AnsibleConfig.password_hasUppercase) { + validity = (/[A-Z]/.test(ctrl.$modelValue)); + ctrl.$setValidity('hasUppercase', validity); + } + if ($AnsibleConfig.password_hasNumber) { + validity = (/[0-9]/.test(ctrl.$modelValue)); + ctrl.$setValidity('hasNumber', validity); + } + if ($AnsibleConfig.password_hasSymbol) { + validity = (/[\\#@$-/:-?{-~!"^_`\[\]]/.test(ctrl.$modelValue)); + ctrl.$setValidity('hasSymbol', validity); + } + } else { + validity = true; + if ($AnsibleConfig.password_length) { + ctrl.$setValidity('password_length', validity); + } + if ($AnsibleConfig.password_hasLowercase) { + ctrl.$setValidity('hasLowercase', validity); + } + if ($AnsibleConfig.password_hasUppercase) { + ctrl.$setValidity('hasUppercase', validity); + } + if ($AnsibleConfig.password_hasNumber) { + ctrl.$setValidity('hasNumber', validity); + } + if ($AnsibleConfig.password_hasSymbol) { + ctrl.$setValidity('hasSymbol', validity); + } + } + if (!scope.$$phase) { + scope.$digest(); + } + }); + } + }; +}]) -.directive('surveyCheckboxes', function(){ +.directive('surveyCheckboxes', function() { return { restrict: 'E', require: 'ngModel', @@ -131,23 +132,23 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) template: '
' + ''+ + '' + + '{{option.value}}' + + '' + '
', - link: function(scope, element, attrs, ctrl){ - scope.cbModel= {}; + link: function(scope, element, attrs, ctrl) { + scope.cbModel = {}; ctrl.$setValidity('reqCheck', true); - angular.forEach(scope.ngModel.value, function(value){ + angular.forEach(scope.ngModel.value, function(value) { scope.cbModel[value] = true; }); - if(scope.ngModel.required===true && scope.ngModel.value.length===0){ + if (scope.ngModel.required === true && scope.ngModel.value.length === 0) { ctrl.$setValidity('reqCheck', false); } - ctrl.$parsers.unshift(function(viewValue){ + ctrl.$parsers.unshift(function(viewValue) { for (var c in scope.cbModel) { if (scope.cbModel[c]) { ctrl.$setValidity('checkbox', true); @@ -158,21 +159,20 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) return viewValue; }); - scope.update = function(){ + scope.update = function() { var val = []; - angular.forEach(scope.cbModel, function(v,k){ + angular.forEach(scope.cbModel, function(v, k) { if (v) { val.push(k); } }); - if (val.length>0){ + if (val.length > 0) { scope.ngModel.value = val; scope.$parent[scope.ngModel.name] = val; ctrl.$setValidity('checkbox', true); ctrl.$setValidity('reqCheck', true); - } - else if(scope.ngModel.required===true){ - ctrl.$setValidity('checkbox' , false); + } else if (scope.ngModel.required === true) { + ctrl.$setValidity('checkbox', false); } }; } @@ -180,883 +180,856 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) }) - .directive('awSurveyQuestion', function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var values = viewValue.split(" "), - result = "", i; - result += values[0].charAt(0).toUpperCase() + values[0].substr(1) + ' '; - for (i = 1; i < values.length; i++){ - result += values[i] + ' '; - } - result = result.trim(); - if (result !== viewValue) { - ctrl.$setViewValue(result); - ctrl.$render(); - } - return result; - }); - } - }; - }) +.directive('awSurveyQuestion', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var values = viewValue.split(" "), + result = "", + i; + result += values[0].charAt(0).toUpperCase() + values[0].substr(1) + ' '; + for (i = 1; i < values.length; i++) { + result += values[i] + ' '; + } + result = result.trim(); + if (result !== viewValue) { + ctrl.$setViewValue(result); + ctrl.$render(); + } + return result; + }); + } + }; +}) - .directive('awMin', ['Empty', function (Empty) { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elem, attr, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity; - if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) { - ctrl.$setValidity('awMin', false); - return viewValue; - } else { - ctrl.$setValidity('awMin', true); - return viewValue; - } - }); - } - }; - }]) - - .directive('awMax', ['Empty', function (Empty) { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elem, attr, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var max = (attr.awMax) ? scope.$eval(attr.awMax) : Infinity; - if (!Empty(max) && !Empty(viewValue) && Number(viewValue) > max) { - ctrl.$setValidity('awMax', false); - return viewValue; - } else { - ctrl.$setValidity('awMax', true); - return viewValue; - } - }); - } - }; - }]) - - .directive('smartFloat', function() { - var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/; - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function(viewValue) { - if (FLOAT_REGEXP.test(viewValue)) { - ctrl.$setValidity('float', true); - return parseFloat(viewValue.replace(',', '.')); - } else { - ctrl.$setValidity('float', false); - return undefined; - } - }); - } - }; - }) - - // integer Validate that input is of type integer. Taken from Angular developer - // guide, form examples. Add min and max directives, and this will check - // entered values is within the range. - // - // Use input type of 'text'. Use of 'number' casuses browser validation to - // override/interfere with this directive. - .directive('integer', function() { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function(viewValue) { - ctrl.$setValidity('min', true); - ctrl.$setValidity('max', true); - if (/^\-?\d*$/.test(viewValue)) { - // it is valid - ctrl.$setValidity('integer', true); - if ( viewValue === '-' || viewValue === '-0' || viewValue === null) { - ctrl.$setValidity('integer', false); - return viewValue; - } - if (elm.attr('min') && - parseInt(viewValue,10) < parseInt(elm.attr('min'),10) ) { - ctrl.$setValidity('min', false); - return viewValue; - } - if ( elm.attr('max') && ( parseInt(viewValue,10) > parseInt(elm.attr('max'),10) ) ) { - ctrl.$setValidity('max', false); - return viewValue; - } - return viewValue; - } - // Invalid, return undefined (no model update) - ctrl.$setValidity('integer', false); +.directive('awMin', ['Empty', function(Empty) { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elem, attr, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity; + if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) { + ctrl.$setValidity('awMin', false); return viewValue; - }); - } - }; - }) + } else { + ctrl.$setValidity('awMin', true); + return viewValue; + } + }); + } + }; +}]) - //the awSurveyVariableName directive checks if the field contains any spaces. - // this could be elaborated in the future for other things we want to check this field against - .directive('awSurveyVariableName', function() { - var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$setValidity('required', true); // we only want the error message for incorrect characters to be displayed - ctrl.$parsers.unshift(function(viewValue) { - if(viewValue.length !== 0){ - if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces - ctrl.$setValidity('variable', true); - return viewValue; - } - else{ - ctrl.$setValidity('variable', false); // spaces found, therefore throw error. - return viewValue; - } - } - else{ - ctrl.$setValidity('variable', true); // spaces found, therefore throw error. +.directive('awMax', ['Empty', function(Empty) { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elem, attr, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var max = (attr.awMax) ? scope.$eval(attr.awMax) : Infinity; + if (!Empty(max) && !Empty(viewValue) && Number(viewValue) > max) { + ctrl.$setValidity('awMax', false); + return viewValue; + } else { + ctrl.$setValidity('awMax', true); + return viewValue; + } + }); + } + }; +}]) + +.directive('smartFloat', function() { + var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/; + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + if (FLOAT_REGEXP.test(viewValue)) { + ctrl.$setValidity('float', true); + return parseFloat(viewValue.replace(',', '.')); + } else { + ctrl.$setValidity('float', false); + return undefined; + } + }); + } + }; +}) + +// integer Validate that input is of type integer. Taken from Angular developer +// guide, form examples. Add min and max directives, and this will check +// entered values is within the range. +// +// Use input type of 'text'. Use of 'number' casuses browser validation to +// override/interfere with this directive. +.directive('integer', function() { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + ctrl.$setValidity('min', true); + ctrl.$setValidity('max', true); + if (/^\-?\d*$/.test(viewValue)) { + // it is valid + ctrl.$setValidity('integer', true); + if (viewValue === '-' || viewValue === '-0' || viewValue === null) { + ctrl.$setValidity('integer', false); return viewValue; - } - }); + } + if (elm.attr('min') && + parseInt(viewValue, 10) < parseInt(elm.attr('min'), 10)) { + ctrl.$setValidity('min', false); + return viewValue; + } + if (elm.attr('max') && (parseInt(viewValue, 10) > parseInt(elm.attr('max'), 10))) { + ctrl.$setValidity('max', false); + return viewValue; + } + return viewValue; + } + // Invalid, return undefined (no model update) + ctrl.$setValidity('integer', false); + return viewValue; + }); + } + }; +}) + +//the awSurveyVariableName directive checks if the field contains any spaces. +// this could be elaborated in the future for other things we want to check this field against +.directive('awSurveyVariableName', function() { + var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$setValidity('required', true); // we only want the error message for incorrect characters to be displayed + ctrl.$parsers.unshift(function(viewValue) { + if (viewValue.length !== 0) { + if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces + ctrl.$setValidity('variable', true); + return viewValue; + } else { + ctrl.$setValidity('variable', false); // spaces found, therefore throw error. + return viewValue; + } + } else { + ctrl.$setValidity('variable', true); // spaces found, therefore throw error. + return viewValue; + } + }); + } + }; +}) + +// +// awRequiredWhen: { reqExpression: "", init: "true|false" } +// +// Make a field required conditionally using an expression. If the expression evaluates to true, the +// field will be required. Otherwise, the required attribute will be removed. +// +.directive('awRequiredWhen', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + + function updateRequired() { + var isRequired = scope.$eval(attrs.awRequiredWhen); + + var viewValue = elm.val(), + label, validity = true; + label = $(elm).closest('.form-group').find('label').first(); + + if (isRequired && (elm.attr('required') === null || elm.attr('required') === undefined)) { + $(elm).attr('required', 'required'); + $(label).removeClass('prepend-asterisk').addClass('prepend-asterisk'); + } else if (!isRequired) { + elm.removeAttr('required'); + if (!attrs.awrequiredAlwaysShowAsterisk) { + $(label).removeClass('prepend-asterisk'); + } + } + if (isRequired && (viewValue === undefined || viewValue === null || viewValue === '')) { + validity = false; + } + ctrl.$setValidity('required', validity); } - }; - }) - // - // awRequiredWhen: { reqExpression: "", init: "true|false" } - // - // Make a field required conditionally using an expression. If the expression evaluates to true, the - // field will be required. Otherwise, the required attribute will be removed. - // - .directive('awRequiredWhen', function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { + scope.$watchGroup([attrs.awRequiredWhen, $(elm).attr('name')], function() { + // watch for the aw-required-when expression to change value + updateRequired(); + }); - function updateRequired () { - var isRequired = scope.$eval(attrs.awRequiredWhen); + if (attrs.awrequiredInit !== undefined && attrs.awrequiredInit !== null) { + // We already set a watcher on the attribute above so no need to call updateRequired() in here + scope[attrs.awRequiredWhen] = attrs.awrequiredInit; + } - var viewValue = elm.val(), label, validity = true; - label = $(elm).closest('.form-group').find('label').first(); + } + }; +}) - if ( isRequired && (elm.attr('required') === null || elm.attr('required') === undefined) ) { - $(elm).attr('required','required'); - $(label).removeClass('prepend-asterisk').addClass('prepend-asterisk'); - } - else if (!isRequired) { - elm.removeAttr('required'); - if(!attrs.awrequiredAlwaysShowAsterisk) { - $(label).removeClass('prepend-asterisk'); - } - } - if (isRequired && (viewValue === undefined || viewValue === null || viewValue === '')) { +// awPlaceholder: Dynamic placeholder set to a scope variable you want watched. +// Value will be place in field placeholder attribute. +.directive('awPlaceholder', [function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs) { + $(elm).attr('placeholder', scope[attrs.awPlaceholder]); + scope.$watch(attrs.awPlaceholder, function(newVal) { + $(elm).attr('placeholder', newVal); + }); + } + }; +}]) + +// lookup Validate lookup value against API +.directive('awlookup', ['Rest', 'GetBasePath', '$q', function(Rest, GetBasePath, $q) { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + + let query, + basePath, + defer = $q.defer(); + + // query the API to see if field value corresponds to a valid resource + // .ng-pending will be applied to the directive element while the request is outstanding + ctrl.$asyncValidators.validResource = function(modelValue, viewValue) { + if (viewValue) { + basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath'); + query = elm.attr('data-query'); + query = query.replace(/\:value/, encodeURI(viewValue)); + Rest.setUrl(`${basePath}${query}`); + // https://github.com/ansible/ansible-tower/issues/3549 + // capturing both success/failure conditions in .then() promise + // when #3549 is resolved, this will need to be partitioned into success/error or then/catch blocks + return Rest.get() + .then((res) => { + if (res.data.results.length > 0) { + scope[elm.attr('data-source')] = res.data.results[0].id; + ctrl.$setValidity('awlookup', true); + defer.resolve(true); + } else { + scope[elm.attr('data-source')] = null; + ctrl.$setValidity('awlookup', false); + defer.resolve(false); + } + }); + } + return defer.promise; + }; + } + }; +}]) + +// +// awValidUrl +// +.directive('awValidUrl', [function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + var validity = true, + rgx, rgx2; + if (viewValue !== '') { + ctrl.$setValidity('required', true); + rgx = /^(https|http|ssh)\:\/\//; + rgx2 = /\@/g; + if (!rgx.test(viewValue) || rgx2.test(viewValue)) { validity = false; } - ctrl.$setValidity('required', validity); } + ctrl.$setValidity('awvalidurl', validity); - scope.$watchGroup([attrs.awRequiredWhen, $(elm).attr('name')], function() { - // watch for the aw-required-when expression to change value - updateRequired(); - }); - - if (attrs.awrequiredInit !== undefined && attrs.awrequiredInit !== null) { - // We already set a watcher on the attribute above so no need to call updateRequired() in here - scope[attrs.awRequiredWhen] = attrs.awrequiredInit; - } + return viewValue; + }); + } + }; +}]) +/* + * Enable TB tooltips. To add a tooltip to an element, include the following directive in + * the element's attributes: + * + * aw-tool-tip="<< tooltip text here >>" + * + * Include the standard TB data-XXX attributes to controll a tooltip's appearance. We will + * default placement to the left and delay to the config setting. + */ +.directive('awToolTip', [function() { + return { + link: function(scope, element, attrs) { + var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : { show: 500, hide: 100 }, + placement, + stateChangeWatcher; + if (attrs.awTipPlacement) { + placement = attrs.awTipPlacement; + } else { + placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left'; } - }; - }) - // awPlaceholder: Dynamic placeholder set to a scope variable you want watched. - // Value will be place in field placeholder attribute. - .directive('awPlaceholder', [ function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs) { - $(elm).attr('placeholder', scope[attrs.awPlaceholder]); - scope.$watch(attrs.awPlaceholder, function(newVal) { - $(elm).attr('placeholder',newVal); - }); + var template, custom_class; + if (attrs.tooltipInnerClass || attrs.tooltipinnerclass) { + custom_class = attrs.tooltipInnerClass || attrs.tooltipinnerclass; + template = ''; + } else { + template = ''; } - }; - }]) - // lookup Validate lookup value against API - // - .directive('awlookup', ['Rest', '$timeout', function(Rest, $timeout) { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - - var restTimeout; - - ctrl.$parsers.unshift( function(viewValue) { - if (viewValue !== '' && viewValue !== null) { - var url = elm.attr('data-url'); - url = url.replace(/\:value/, encodeURI(viewValue)); - scope[elm.attr('data-source')] = null; - if(restTimeout) { - $timeout.cancel(restTimeout); - } - restTimeout = $timeout( function(){ - Rest.setUrl(url); - Rest.get().then( function(data) { - var results = data.data.results; - if (results.length > 0) { - scope[elm.attr('data-source')] = results[0].id; - - // For user lookups the API endpoint doesn't - // have a `name` property, so this is `undefined` - // which causes the input to clear after typing - // a valid value O_o - // - // Only assign if there is a value, so that we avoid - // this situation. - // - // TODO: Evaluate if assigning name on the scope is - // even necessary at all. - // - if (!_.isEmpty(results[0].name)) { - scope[elm.attr('name')] = results[0].name; - } - - ctrl.$setValidity('required', true); - ctrl.$setValidity('awlookup', true); - return viewValue; - } - ctrl.$setValidity('required', true); - ctrl.$setValidity('awlookup', false); - return undefined; - }); - }, 750); - } - else { - if(restTimeout) { - $timeout.cancel(restTimeout); - } - ctrl.$setValidity('awlookup', true); - scope[elm.attr('data-source')] = null; - } - return viewValue; - }); - } - }; - }]) - - // - // awValidUrl - // - .directive('awValidUrl', [ function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift( function(viewValue) { - var validity = true, rgx, rgx2; - if (viewValue !== '') { - ctrl.$setValidity('required', true); - rgx = /^(https|http|ssh)\:\/\//; - rgx2 = /\@/g; - if (!rgx.test(viewValue) || rgx2.test(viewValue)) { - validity = false; - } - } - ctrl.$setValidity('awvalidurl', validity); - - return viewValue; - }); - } - }; - }]) - - /* - * Enable TB tooltips. To add a tooltip to an element, include the following directive in - * the element's attributes: - * - * aw-tool-tip="<< tooltip text here >>" - * - * Include the standard TB data-XXX attributes to controll a tooltip's appearance. We will - * default placement to the left and delay to the config setting. - */ - .directive('awToolTip', [ function() { - return { - link: function(scope, element, attrs) { - var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : {show: 500, hide: 100}, - placement, - stateChangeWatcher; - if (attrs.awTipPlacement) { - placement = attrs.awTipPlacement; + // This block helps clean up tooltips that may get orphaned by a click event + $(element).on('mouseenter', function() { + if (stateChangeWatcher) { + // Un-bind - we don't want a bunch of listeners firing + stateChangeWatcher(); } - else { - placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left'; - } - - var template, custom_class; - if (attrs.tooltipInnerClass || attrs.tooltipinnerclass) { - custom_class = attrs.tooltipInnerClass || attrs.tooltipinnerclass; - template = ''; - } else { - template = ''; - } - - // This block helps clean up tooltips that may get orphaned by a click event - $(element).on('mouseenter', function() { - if(stateChangeWatcher) { - // Un-bind - we don't want a bunch of listeners firing - stateChangeWatcher(); - } - stateChangeWatcher = scope.$on('$stateChangeStart', function() { - // Go ahead and force the tooltip setTimeout to expire (if it hasn't already fired) - $(element).tooltip('hide'); - // Clean up any existing tooltips including this one - $('.tooltip').each(function() { - $(this).remove(); - }); - }); - }); - - $(element).on('hidden.bs.tooltip', function( ) { - // TB3RC1 is leaving behind tooltip
elements. This will remove them - // after a tooltip fades away. If not, they lay overtop of other elements and - // honk up the page. + stateChangeWatcher = scope.$on('$stateChangeStart', function() { + // Go ahead and force the tooltip setTimeout to expire (if it hasn't already fired) + $(element).tooltip('hide'); + // Clean up any existing tooltips including this one $('.tooltip').each(function() { $(this).remove(); }); }); - - $(element).tooltip({ - placement: placement, - delay: delay, - html: true, - title: attrs.awToolTip, - container: 'body', - trigger: 'hover', - template: template - }); - - if (attrs.tipWatch) { - // Add dataTipWatch: 'variable_name' - scope.$watch(attrs.tipWatch, function(newVal, oldVal) { - if (newVal !== oldVal) { - // Where did fixTitle come from?: - // http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click - $(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle'); - } - }); - } - } - }; - }]) - - /* - * Enable TB pop-overs. To add a pop-over to an element, include the following directive in - * the element's attributes: - * - * aw-pop-over="<< pop-over html here >>" - * - * Include the standard TB data-XXX attributes to controll the pop-over's appearance. We will - * default placement to the left, delay to 0 seconds, content type to HTML, and title to 'Help'. - */ - .directive('awPopOver', ['$compile', function($compile) { - return function(scope, element, attrs) { - var placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left', - title = (attrs.overTitle) ? attrs.overTitle : (attrs.popoverTitle) ? attrs.popoverTitle : 'Help', - container = (attrs.container !== undefined) ? attrs.container : false, - trigger = (attrs.trigger !== undefined) ? attrs.trigger : 'manual', - template = '', - id_to_close = ""; - - if (element[0].id) { - template = ''; - } - - scope.triggerPopover = function(e){ - showPopover(e); - }; - - if (attrs.awPopOverWatch) { - $(element).popover({ - placement: placement, - delay: 0, - title: title, - content: function() { - return scope[attrs.awPopOverWatch]; - }, - trigger: trigger, - html: true, - container: container, - template: template - }); - } else { - $(element).popover({ - placement: placement, - delay: 0, - title: title, - content: attrs.awPopOver, - trigger: trigger, - html: true, - container: container, - template: template - }); - } - $(element).attr('tabindex',-1); - - $(element).one('click', showPopover); - - function bindPopoverDismiss() { - $('body').one('click.popover' + id_to_close, function(e) { - if ($(e.target).parents(id_to_close).length === 0) { - // case: you clicked to open the popover and then you - // clicked outside of it...hide it. - $(element).popover('hide'); - } else { - // case: you clicked to open the popover and then you - // clicked inside the popover - bindPopoverDismiss(); - } - }); - } - - $(element).on('shown.bs.popover', function() { - bindPopoverDismiss(); - $(document).on('keydown.popover', dismissOnEsc); }); - $(element).on('hidden.bs.popover', function() { - $(element).off('click', dismissPopover); - $(element).off('click', showPopover); - $('body').off('click.popover.' + id_to_close); - $(element).one('click', showPopover); - $(document).off('keydown.popover', dismissOnEsc); - }); - - function showPopover(e) { - e.stopPropagation(); - - var self = $(element); - - // remove tool-tip - try { - element.tooltip('hide'); - } - catch(ex) { - // ignore - } - - // this is called on the help-link (over and over again) - $('.help-link, .help-link-white').each( function() { - if (self.attr('id') !== $(this).attr('id')) { - try { - // not sure what this does different than the method above - $(this).popover('hide'); - } - catch(e) { - // ignore - } - } - }); - - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 + $(element).on('hidden.bs.tooltip', function() { + // TB3RC1 is leaving behind tooltip
elements. This will remove them + // after a tooltip fades away. If not, they lay overtop of other elements and + // honk up the page. + $('.tooltip').each(function() { $(this).remove(); }); - $('.tooltip').each( function() { - // close any lingering tool tips - $(this).hide(); - }); + }); - // set id_to_close of the actual open element - id_to_close = "#" + $(element).attr('id') + "_popover_container"; + $(element).tooltip({ + placement: placement, + delay: delay, + html: true, + title: attrs.awToolTip, + container: 'body', + trigger: 'hover', + template: template + }); - // $(element).one('click', dismissPopover); - - $(element).popover('toggle'); - - $('.popover').each(function() { - $compile($(this))(scope); //make nested directives work! + if (attrs.tipWatch) { + // Add dataTipWatch: 'variable_name' + scope.$watch(attrs.tipWatch, function(newVal, oldVal) { + if (newVal !== oldVal) { + // Where did fixTitle come from?: + // http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click + $(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle'); + } }); } + } + }; +}]) - function dismissPopover(e) { - e.stopPropagation(); - $(element).popover('hide'); - } +/* + * Enable TB pop-overs. To add a pop-over to an element, include the following directive in + * the element's attributes: + * + * aw-pop-over="<< pop-over html here >>" + * + * Include the standard TB data-XXX attributes to controll the pop-over's appearance. We will + * default placement to the left, delay to 0 seconds, content type to HTML, and title to 'Help'. + */ +.directive('awPopOver', ['$compile', function($compile) { + return function(scope, element, attrs) { + var placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left', + title = (attrs.overTitle) ? attrs.overTitle : (attrs.popoverTitle) ? attrs.popoverTitle : 'Help', + container = (attrs.container !== undefined) ? attrs.container : false, + trigger = (attrs.trigger !== undefined) ? attrs.trigger : 'manual', + template = '', + id_to_close = ""; - function dismissOnEsc(e) { - if (e.keyCode === 27) { + if (element[0].id) { + template = ''; + } + + scope.triggerPopover = function(e) { + showPopover(e); + }; + + if (attrs.awPopOverWatch) { + $(element).popover({ + placement: placement, + delay: 0, + title: title, + content: function() { + return scope[attrs.awPopOverWatch]; + }, + trigger: trigger, + html: true, + container: container, + template: template + }); + } else { + $(element).popover({ + placement: placement, + delay: 0, + title: title, + content: attrs.awPopOver, + trigger: trigger, + html: true, + container: container, + template: template + }); + } + $(element).attr('tabindex', -1); + + $(element).one('click', showPopover); + + function bindPopoverDismiss() { + $('body').one('click.popover' + id_to_close, function(e) { + if ($(e.target).parents(id_to_close).length === 0) { + // case: you clicked to open the popover and then you + // clicked outside of it...hide it. $(element).popover('hide'); - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - // $(this).remove(); - }); + } else { + // case: you clicked to open the popover and then you + // clicked inside the popover + bindPopoverDismiss(); } + }); + } + + $(element).on('shown.bs.popover', function() { + bindPopoverDismiss(); + $(document).on('keydown.popover', dismissOnEsc); + }); + + $(element).on('hidden.bs.popover', function() { + $(element).off('click', dismissPopover); + $(element).off('click', showPopover); + $('body').off('click.popover.' + id_to_close); + $(element).one('click', showPopover); + $(document).off('keydown.popover', dismissOnEsc); + }); + + function showPopover(e) { + e.stopPropagation(); + + var self = $(element); + + // remove tool-tip + try { + element.tooltip('hide'); + } catch (ex) { + // ignore } - }; - }]) - - // - // Enable jqueryui slider widget on a numeric input field - // - // - // - .directive('awSlider', [ function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - var name = elm.attr('name'); - $('#' + name + '-slider').slider({ - value: 0, - step: 1, - min: elm.attr('min'), - max: elm.attr('max'), - disabled: (elm.attr('readonly')) ? true : false, - slide: function(e,u) { - ctrl.$setViewValue(u.value); - ctrl.$setValidity('required',true); - ctrl.$setValidity('min', true); - ctrl.$setValidity('max', true); - ctrl.$dirty = true; - ctrl.$render(); - if (!scope.$$phase) { - scope.$digest(); - } + // this is called on the help-link (over and over again) + $('.help-link, .help-link-white').each(function() { + if (self.attr('id') !== $(this).attr('id')) { + try { + // not sure what this does different than the method above + $(this).popover('hide'); + } catch (e) { + // ignore } - }); - - $('#' + name + '-number').change( function() { - $('#' + name + '-slider').slider('value', parseInt($(this).val(),10)); - }); - - } - }; - }]) - - // - // Enable jqueryui spinner widget on a numeric input field - // - // - // - .directive('awSpinner', [ function() { - return { - require: 'ngModel', - link: function(scope, elm, attrs, ctrl) { - var disabled, opts; - disabled = elm.attr('data-disabled'); - opts = { - value: 0, - step: 1, - min: elm.attr('min'), - max: elm.attr('max'), - numberFormat: "d", - disabled: (elm.attr('readonly')) ? true : false, - icons: { - down: "Form-numberInputButton fa fa-angle-down", - up: "Form-numberInputButton fa fa-angle-up" - }, - spin: function(e, u) { - ctrl.$setViewValue(u.value); - ctrl.$setValidity('required',true); - ctrl.$setValidity('min', true); - ctrl.$setValidity('max', true); - ctrl.$dirty = true; - ctrl.$render(); - if (scope.job_templates_form) { - // need a way to find the parent form and mark it dirty - scope.job_templates_form.$dirty = true; - } - if (!scope.$$phase) { - scope.$digest(); - } - } - }; - if (disabled) { - opts.disabled = true; } - $(elm).spinner(opts); - $('.ui-icon').text(''); - $(".ui-icon").removeClass('ui-icon ui-icon-triangle-1-n ui-icon-triangle-1-s'); - $(elm).on("click", function () { - $(elm).select(); + }); + + $('.popover').each(function() { + // remove lingering popover
. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each(function() { + // close any lingering tool tips + $(this).hide(); + }); + + // set id_to_close of the actual open element + id_to_close = "#" + $(element).attr('id') + "_popover_container"; + + // $(element).one('click', dismissPopover); + + $(element).popover('toggle'); + + $('.popover').each(function() { + $compile($(this))(scope); //make nested directives work! + }); + } + + function dismissPopover(e) { + e.stopPropagation(); + $(element).popover('hide'); + } + + function dismissOnEsc(e) { + if (e.keyCode === 27) { + $(element).popover('hide'); + $('.popover').each(function() { + // remove lingering popover
. Seems to be a bug in TB3 RC1 + // $(this).remove(); }); } - }; - }]) + } - // - // awRefresh - // - // Creates a timer to call scope.refresh(iterator) ever N seconds, where - // N is a setting in config.js - // - .directive('awRefresh', [ '$rootScope', function($rootScope) { - return { - link: function(scope) { - function msg() { - var num = '' + scope.refreshCnt; - while (num.length < 2) { - num = '0' + num; - } - return 'Refresh in ' + num + ' sec.'; - } - scope.refreshCnt = $AnsibleConfig.refresh_rate; - scope.refreshMsg = msg(); - if ($rootScope.timer) { - clearInterval($rootScope.timer); - } - $rootScope.timer = setInterval( function() { - scope.refreshCnt--; - if (scope.refreshCnt <= 0) { - scope.refresh(); - scope.refreshCnt = $AnsibleConfig.refresh_rate; - } - scope.refreshMsg = msg(); + }; +}]) + +// +// Enable jqueryui slider widget on a numeric input field +// +// +// +.directive('awSlider', [function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + var name = elm.attr('name'); + $('#' + name + '-slider').slider({ + value: 0, + step: 1, + min: elm.attr('min'), + max: elm.attr('max'), + disabled: (elm.attr('readonly')) ? true : false, + slide: function(e, u) { + ctrl.$setViewValue(u.value); + ctrl.$setValidity('required', true); + ctrl.$setValidity('min', true); + ctrl.$setValidity('max', true); + ctrl.$dirty = true; + ctrl.$render(); if (!scope.$$phase) { scope.$digest(); } - }, 1000); + } + }); + + $('#' + name + '-number').change(function() { + $('#' + name + '-slider').slider('value', parseInt($(this).val(), 10)); + }); + + } + }; +}]) + +// +// Enable jqueryui spinner widget on a numeric input field +// +// +// +.directive('awSpinner', [function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + var disabled, opts; + disabled = elm.attr('data-disabled'); + opts = { + value: 0, + step: 1, + min: elm.attr('min'), + max: elm.attr('max'), + numberFormat: "d", + disabled: (elm.attr('readonly')) ? true : false, + icons: { + down: "Form-numberInputButton fa fa-angle-down", + up: "Form-numberInputButton fa fa-angle-up" + }, + spin: function(e, u) { + ctrl.$setViewValue(u.value); + ctrl.$setValidity('required', true); + ctrl.$setValidity('min', true); + ctrl.$setValidity('max', true); + ctrl.$dirty = true; + ctrl.$render(); + if (scope.job_templates_form) { + // need a way to find the parent form and mark it dirty + scope.job_templates_form.$dirty = true; + } + if (!scope.$$phase) { + scope.$digest(); + } + } + }; + if (disabled) { + opts.disabled = true; } - }; - }]) + $(elm).spinner(opts); + $('.ui-icon').text(''); + $(".ui-icon").removeClass('ui-icon ui-icon-triangle-1-n ui-icon-triangle-1-s'); + $(elm).on("click", function() { + $(elm).select(); + }); + } + }; +}]) - /* - * Make an element draggable. Used on inventory groups tree. - * - * awDraggable: boolean || {{ expression }} - * - */ - .directive('awDraggable', [ function() { - return function(scope, element, attrs) { - - if (attrs.awDraggable === "true") { - var containment = attrs.containment; //provide dataContainment:"#id" - $(element).draggable({ - containment: containment, - scroll: true, - revert: "invalid", - helper: "clone", - start: function(e, ui) { - ui.helper.addClass('draggable-clone'); - }, - zIndex: 100, - cursorAt: { left: -1 } - }); +// +// awRefresh +// +// Creates a timer to call scope.refresh(iterator) ever N seconds, where +// N is a setting in config.js +// +.directive('awRefresh', ['$rootScope', function($rootScope) { + return { + link: function(scope) { + function msg() { + var num = '' + scope.refreshCnt; + while (num.length < 2) { + num = '0' + num; + } + return 'Refresh in ' + num + ' sec.'; } - }; - }]) + scope.refreshCnt = $AnsibleConfig.refresh_rate; + scope.refreshMsg = msg(); + if ($rootScope.timer) { + clearInterval($rootScope.timer); + } + $rootScope.timer = setInterval(function() { + scope.refreshCnt--; + if (scope.refreshCnt <= 0) { + scope.refresh(); + scope.refreshCnt = $AnsibleConfig.refresh_rate; + } + scope.refreshMsg = msg(); + if (!scope.$$phase) { + scope.$digest(); + } + }, 1000); + } + }; +}]) - /* - * Make an element droppable- it can receive draggable elements - * - * awDroppable: boolean || {{ expression }} - * - */ - .directive('awDroppable', ['Find', function(Find) { - return function(scope, element, attrs) { - var node; - if (attrs.awDroppable === "true") { - $(element).droppable({ - // the following is inventory specific accept checking and - // drop processing. - accept: function(draggable) { - if (draggable.attr('data-type') === 'group') { - // Dropped a group - if ($(this).attr('data-group-id') === draggable.attr('data-group-id')) { - // No dropping a node onto itself (or a copy) - return false; - } - // No dropping a node into a group that already has the node - node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'),10) }); - if (node) { - var group = parseInt(draggable.attr('data-group-id'),10), - found = false, i; - // For whatever reason indexOf() would not work... - for (i=0; i < node.children.length; i++) { - if (node.children[i] === group) { - found = true; - break; - } - } - return (found) ? false : true; - } +/* + * Make an element draggable. Used on inventory groups tree. + * + * awDraggable: boolean || {{ expression }} + * + */ +.directive('awDraggable', [function() { + return function(scope, element, attrs) { + + if (attrs.awDraggable === "true") { + var containment = attrs.containment; //provide dataContainment:"#id" + $(element).draggable({ + containment: containment, + scroll: true, + revert: "invalid", + helper: "clone", + start: function(e, ui) { + ui.helper.addClass('draggable-clone'); + }, + zIndex: 100, + cursorAt: { left: -1 } + }); + } + }; +}]) + +/* + * Make an element droppable- it can receive draggable elements + * + * awDroppable: boolean || {{ expression }} + * + */ +.directive('awDroppable', ['Find', function(Find) { + return function(scope, element, attrs) { + var node; + if (attrs.awDroppable === "true") { + $(element).droppable({ + // the following is inventory specific accept checking and + // drop processing. + accept: function(draggable) { + if (draggable.attr('data-type') === 'group') { + // Dropped a group + if ($(this).attr('data-group-id') === draggable.attr('data-group-id')) { + // No dropping a node onto itself (or a copy) return false; } - if (draggable.attr('data-type') === 'host') { - // Dropped a host - node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'),10) }); - return (node.id > 1) ? true : false; + // No dropping a node into a group that already has the node + node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'), 10) }); + if (node) { + var group = parseInt(draggable.attr('data-group-id'), 10), + found = false, + i; + // For whatever reason indexOf() would not work... + for (i = 0; i < node.children.length; i++) { + if (node.children[i] === group) { + found = true; + break; + } + } + return (found) ? false : true; } return false; - }, - over: function() { - $(this).addClass('droppable-hover'); - }, - out: function() { - $(this).removeClass('droppable-hover'); - }, - drop: function(e, ui) { - // Drag-n-drop succeeded. Trigger a response from the inventory.edit controller - $(this).removeClass('droppable-hover'); - if (ui.draggable.attr('data-type') === 'group') { - scope.$emit('CopyMoveGroup', parseInt(ui.draggable.attr('data-tree-id'),10), - parseInt($(this).attr('data-tree-id'),10)); - } - else if (ui.draggable.attr('data-type') === 'host') { - scope.$emit('CopyMoveHost', parseInt($(this).attr('data-tree-id'),10), - parseInt(ui.draggable.attr('data-host-id'),10)); - } - }, - tolerance: 'pointer' + } + if (draggable.attr('data-type') === 'host') { + // Dropped a host + node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'), 10) }); + return (node.id > 1) ? true : false; + } + return false; + }, + over: function() { + $(this).addClass('droppable-hover'); + }, + out: function() { + $(this).removeClass('droppable-hover'); + }, + drop: function(e, ui) { + // Drag-n-drop succeeded. Trigger a response from the inventory.edit controller + $(this).removeClass('droppable-hover'); + if (ui.draggable.attr('data-type') === 'group') { + scope.$emit('CopyMoveGroup', parseInt(ui.draggable.attr('data-tree-id'), 10), + parseInt($(this).attr('data-tree-id'), 10)); + } else if (ui.draggable.attr('data-type') === 'host') { + scope.$emit('CopyMoveHost', parseInt($(this).attr('data-tree-id'), 10), + parseInt(ui.draggable.attr('data-host-id'), 10)); + } + }, + tolerance: 'pointer' + }); + } + }; +}]) + + +.directive('awAccordion', ['Empty', '$location', 'Store', function(Empty, $location, Store) { + return function(scope, element, attrs) { + var active, + list = Store('accordions'), + id, base; + + if (!Empty(attrs.openFirst)) { + active = 0; + } else { + // Look in storage for last active panel + if (list) { + id = $(element).attr('id'); + base = ($location.path().replace(/^\//, '').split('/')[0]); + list.every(function(elem) { + if (elem.base === base && elem.id === id) { + active = elem.active; + return false; + } + return true; }); } - }; - }]) + active = (Empty(active)) ? 0 : active; + } - - .directive('awAccordion', ['Empty', '$location', 'Store', function(Empty, $location, Store) { - return function(scope, element, attrs) { - var active, - list = Store('accordions'), - id, base; - - if (!Empty(attrs.openFirst)) { - active = 0; - } - else { - // Look in storage for last active panel - if (list) { - id = $(element).attr('id'); - base = ($location.path().replace(/^\//, '').split('/')[0]); - list.every(function(elem) { - if (elem.base === base && elem.id === id) { - active = elem.active; - return false; - } - return true; + $(element).accordion({ + collapsible: true, + heightStyle: "content", + active: active, + activate: function() { + // When a panel is activated update storage + var active = $(this).accordion('option', 'active'), + id = $(this).attr('id'), + base = ($location.path().replace(/^\//, '').split('/')[0]), + list = Store('accordions'), + found = false; + if (!list) { + list = []; + } + list.every(function(elem) { + if (elem.base === base && elem.id === id) { + elem.active = active; + found = true; + return false; + } + return true; + }); + if (found === false) { + list.push({ + base: base, + id: id, + active: active }); } - active = (Empty(active)) ? 0 : active; + Store('accordions', list); + } + }); + }; +}]) + +// Toggle switch inspired by http://www.bootply.com/92189 +.directive('awToggleButton', [function() { + return function(scope, element) { + $(element).click(function() { + var next, choice; + $(this).find('.btn').toggleClass('active'); + if ($(this).find('.btn-primary').size() > 0) { + $(this).find('.btn').toggleClass('btn-primary'); + } + if ($(this).find('.btn-danger').size() > 0) { + $(this).find('.btn').toggleClass('btn-danger'); + } + if ($(this).find('.btn-success').size() > 0) { + $(this).find('.btn').toggleClass('btn-success'); + } + if ($(this).find('.btn-info').size() > 0) { + $(this).find('.btn').toggleClass('btn-info'); + } + $(this).find('.btn').toggleClass('btn-default'); + + // Add data-after-toggle="functionName" to the btn-group, and we'll + // execute here. The newly active choice is passed as a parameter. + if ($(this).attr('data-after-toggle')) { + next = $(this).attr('data-after-toggle'); + choice = $(this).find('.active').text(); + setTimeout(function() { + scope.$apply(function() { + scope[next](choice); + }); + }); } - $(element).accordion({ - collapsible: true, - heightStyle: "content", - active: active, - activate: function() { - // When a panel is activated update storage - var active = $(this).accordion('option', 'active'), - id = $(this).attr('id'), - base = ($location.path().replace(/^\//, '').split('/')[0]), - list = Store('accordions'), - found = false; - if (!list) { - list = []; + }); + }; +}]) + +// +// Support dropping files on an element. Used on credentials page for SSH/RSA private keys +// Inspired by https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications +// +.directive('awDropFile', ['Alert', function(Alert) { + return { + require: 'ngModel', + link: function(scope, element, attrs, ctrl) { + $(element).on('dragenter', function(e) { + e.stopPropagation(); + e.preventDefault(); + }); + $(element).on('dragover', function(e) { + e.stopPropagation(); + e.preventDefault(); + }); + $(element).on('drop', function(e) { + var dt, files, reader; + e.stopPropagation(); + e.preventDefault(); + dt = e.originalEvent.dataTransfer; + files = dt.files; + reader = new FileReader(); + reader.onload = function() { + ctrl.$setViewValue(reader.result); + ctrl.$render(); + ctrl.$setValidity('required', true); + ctrl.$dirty = true; + if (!scope.$$phase) { + scope.$digest(); } - list.every(function(elem) { - if (elem.base === base && elem.id === id) { - elem.active = active; - found = true; - return false; - } - return true; - }); - if (found === false) { - list.push({ - base: base, - id: id, - active: active - }); - } - Store('accordions', list); + }; + reader.onerror = function() { + Alert('Error', 'There was an error reading the selected file.'); + }; + if (files[0].size < 10000) { + reader.readAsText(files[0]); + } else { + Alert('Error', 'There was an error reading the selected file.'); } }); - }; - }]) - - // Toggle switch inspired by http://www.bootply.com/92189 - .directive('awToggleButton', [ function() { - return function(scope, element) { - $(element).click(function() { - var next, choice; - $(this).find('.btn').toggleClass('active'); - if ($(this).find('.btn-primary').size()>0) { - $(this).find('.btn').toggleClass('btn-primary'); - } - if ($(this).find('.btn-danger').size()>0) { - $(this).find('.btn').toggleClass('btn-danger'); - } - if ($(this).find('.btn-success').size()>0) { - $(this).find('.btn').toggleClass('btn-success'); - } - if ($(this).find('.btn-info').size()>0) { - $(this).find('.btn').toggleClass('btn-info'); - } - $(this).find('.btn').toggleClass('btn-default'); - - // Add data-after-toggle="functionName" to the btn-group, and we'll - // execute here. The newly active choice is passed as a parameter. - if ($(this).attr('data-after-toggle')) { - next = $(this).attr('data-after-toggle'); - choice = $(this).find('.active').text(); - setTimeout(function() { - scope.$apply(function() { - scope[next](choice); - }); - }); - } - - }); - }; - }]) - - // - // Support dropping files on an element. Used on credentials page for SSH/RSA private keys - // Inspired by https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications - // - .directive('awDropFile', ['Alert', function(Alert) { - return { - require: 'ngModel', - link: function(scope, element, attrs, ctrl) { - $(element).on('dragenter', function(e) { - e.stopPropagation(); - e.preventDefault(); - }); - $(element).on('dragover', function(e) { - e.stopPropagation(); - e.preventDefault(); - }); - $(element).on('drop', function(e) { - var dt, files, reader; - e.stopPropagation(); - e.preventDefault(); - dt = e.originalEvent.dataTransfer; - files = dt.files; - reader = new FileReader(); - reader.onload = function() { - ctrl.$setViewValue(reader.result); - ctrl.$render(); - ctrl.$setValidity('required',true); - ctrl.$dirty = true; - if (!scope.$$phase) { - scope.$digest(); - } - }; - reader.onerror = function() { - Alert('Error','There was an error reading the selected file.'); - }; - if(files[0].size<10000){ - reader.readAsText(files[0]); - } - else { - Alert('Error','There was an error reading the selected file.'); - } - }); - } - }; - }]); + } + }; +}]); diff --git a/awx/ui/client/src/shared/filters.js b/awx/ui/client/src/shared/filters.js index 2531f1da35..50014c06ce 100644 --- a/awx/ui/client/src/shared/filters.js +++ b/awx/ui/client/src/shared/filters.js @@ -18,6 +18,19 @@ export default angular.module('AWFilters', []) + // Object is empty / undefined / null + .filter('isEmpty', function () { + var key; + return function (obj) { + for (key in obj) { + if (obj.hasOwnProperty(key)) { + return false; + } + } + return true; + }; + }) + // // capitalize -capitalize the first letter of each word // diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 86622485fa..3ed0c224a9 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -67,7 +67,6 @@ * * | Attribute | Description | * | --------- | ----------- | - * | addRequired | true or false. If true, set the required attribute when mode is 'add'. | * | awPopOver | Adds aw-pop-over directive. Set to a string containing the text or html to be evaluated by the directive. | * | awPopOverWatch | Causes the awPopOver directive to add a $scope.$watch on the specified scop variable. When the value of the variable changes the popover text will be updated with the change. | * | awRequiredWhen | Adds aw-required-when directive. Set to an object to be evaluated by the directive. | @@ -79,7 +78,6 @@ * | dataTitle | Used with awPopOver. String value for the title of the popover. | * | default | Default value to place in the field when the form is in 'add' mode. | * | defaultText | Default value to put into a select input. | - * | editRequird | true or false. If true, set the required attribute when mode is 'edit'. | * | falseValue | For radio buttons and checkboxes. Value to set the model to when the checkbox or radio button is not selected. | * | genMD5 | true or false. If true, places the field in an input group with a button that when clicked replaces the field contents with an MD5 has key. Used with host_config_key on the job templates detail page. | * | integer | Adds the integer directive to validate that the value entered is of type integer. Add min and max to supply lower and upper range bounds to the entered value. | @@ -88,7 +86,9 @@ * | ngClick | Adds ng-click directive. Set to the JS expression to be evaluated by ng-click. | * | ngHide | Adds ng-hide directive. Set to the JS expression to be evaluated by ng-hide. | * | ngShow | Adds ng-show directive. Set to the JS expression to be evaluated by ng-show. | + * | readonly | Defaults to false. When true the readonly attribute is set, disallowing changes to field content. | + * | required | boolean. Adds required flag to form field | * | rows | Integer value used to set the row attribute for a textarea. | * | sourceModel | Used in conjunction with sourceField when the data for the field is part of the summary_fields object returned by the API. Set to the name of the summary_fields object that contains the field. For example, the job_templates object returned by the API contains summary_fields.inventory. | * | sourceField | String containing the summary_field.object.field name from the API summary_field object. For example, if a fields should be associated to the summary_fields.inventory.name, set the sourceModel to 'inventory' and the sourceField to 'name'. | @@ -140,13 +140,12 @@ export default angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerator.name]) .factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList', - 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column', + 'Attr', 'Icon', 'Column', 'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon', - 'Store', 'ActionButton', 'getSearchHtml', 'i18n', - function ($rootScope, $location, $compile, GenerateList, SearchWidget, - PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse, - DropDown, Empty, SelectIcon, Store, ActionButton, getSearchHtml, - i18n) { + 'Store', 'ActionButton', '$log', 'i18n', + function ($rootScope, $location, $compile, GenerateList, + Attr, Icon, Column, NavigationLink, HelpCollapse, + DropDown, Empty, SelectIcon, Store, ActionButton, $log, i18n) { return { setForm: function (form) { this.form = form; }, @@ -162,204 +161,15 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat has: function (key) { return (this.form[key] && this.form[key] !== null && this.form[key] !== undefined) ? true : false; }, - - inject: function (form, options) { - // - // Use to inject the form as html into the view. View MUST have an ng-bind for 'htmlTemplate'. - // Returns scope of form. - // - - var element, fld, set, show, self = this; - - if (options.modal) { - if (options.modal_body_id) { - element = angular.element(document.getElementById(options.modal_body_id)); - } else { - // use default dialog - element = angular.element(document.getElementById('form-modal-body')); - } - } else { - if (options.id) { - element = angular.element(document.getElementById(options.id)); - } else { - element = angular.element(document.getElementById('htmlTemplate')); - } - } - - this.mode = options.mode; - this.modal = (options.modal) ? true : false; - this.setForm(form); - - if (options.html) { - element.html(options.html); - } else { - element.html(this.build(options)); - } - - if (options.scope) { - this.scope = options.scope; - } else { - this.scope = element.scope(); - } - - if (options.mode) { - this.scope.mode = options.mode; - } - - if(options.mode === 'edit' && this.form.related && - !_.isEmpty(this.form.related)){ - var tabs = [this.form.name], that = this; - tabs.push(Object.keys(this.form.related)); - tabs = _.flatten(tabs); - _.map(tabs, function(itm){ - that.scope.$parent[itm+"Selected"] = false; - }); - this.scope.$parent[this.form.name+"Selected"] = true; - - - this.scope.$parent.toggleFormTabs = function($event){ - _.map(tabs, function(itm){ - that.scope.$parent[itm+"Selected"] = false; - }); - that.scope.$parent[$event.target.id.split('_tab')[0]+"Selected"] = true; - }; - - } - - for (fld in form.fields) { - this.scope[fld + '_field'] = form.fields[fld]; - this.scope[fld + '_field'].name = fld; - } - - for (fld in form.headerFields){ - this.scope[fld + '_field'] = form.headerFields[fld]; - this.scope[fld + '_field'].name = fld; - } - - $compile(element)(this.scope); - - if (!options.html) { - // Reset the scope to prevent displaying old data from our last visit to this form - for (fld in form.fields) { - this.scope[fld] = null; - } - for (set in form.related) { - this.scope[set] = null; - } - if (((!options.modal) && options.related) || this.form.forceListeners) { - this.addListeners(); - } - if (options.mode === 'add') { - this.applyDefaults(); - } - } - - // Remove any lingering tooltip and popover
elements - $('.tooltip').each(function () { - $(this).remove(); - }); - - $('.popover').each(function () { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - - // Prepend an asterisk to required field label - $('.form-control[required], input[type="radio"][required]').each(function () { - var label, span; - if (Empty($(this).attr('aw-required-when'))) { - label = $(this).closest('.form-group').find('label').first(); - if (label.length > 0) { - span = label.children('span'); - if (span.length > 0 && !span.first().hasClass('prepend-asterisk')) { - span.first().addClass('prepend-asterisk'); - } else if (span.length <= 0 && !label.first().hasClass('prepend-asterisk')) { - label.first().addClass('prepend-asterisk'); - } - } - } - }); - - try { - $('#help-modal').empty().dialog('destroy'); - } catch (e) { - //ignore any errors should the dialog not be initialized - } - - if (options.modal) { - $rootScope.flashMessage = null; - this.scope.formModalActionDisabled = false; - this.scope.formModalInfo = false; //Disable info button for default modal - if (form) { - if (options.modal_title_id) { - this.scope[options.modal_title_id] = (options.mode === 'add') ? form.addTitle : form.editTitle; - } else { - this.scope.formModalHeader = (options.mode === 'add') ? form.addTitle : form.editTitle; //Default title for default modal - } - } - if (options.modal_selector) { - $(options.modal_selector).modal({ - show: true, - backdrop: 'static', - keyboard: true - }); - $(options.modal_selector).on('shown.bs.modal', function () { - $(options.modal_select + ' input:first').focus(); - }); - $(options.modal_selector).on('hidden.bs.modal', function () { - $('.tooltip').each(function () { - // Remove any lingering tooltip and popover
elements - $(this).remove(); - }); - - $('.popover').each(function () { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - }); - } else { - show = (options.show_modal === false) ? false : true; - $('#form-modal').modal({ - show: show, - backdrop: 'static', - keyboard: true - }); - $('#form-modal').on('shown.bs.modal', function () { - $('#form-modal input:first').focus(); - }); - $('#form-modal').on('hidden.bs.modal', function () { - $('.tooltip').each(function () { - // Remove any lingering tooltip and popover
elements - $(this).remove(); - }); - - $('.popover').each(function () { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - }); - } - $(document).bind('keydown', function (e) { - if (e.keyCode === 27) { - if (options.modal_selector) { - $(options.modal_selector).modal('hide'); - } - $('#prompt-modal').modal('hide'); - $('#form-modal').modal('hide'); - } - }); - } - - if (self.scope && !self.scope.$$phase) { - setTimeout(function() { - if (self.scope) { - self.scope.$digest(); - } - }, 100); - } - - return self.scope; - + // Not a very good way to do this + // Form sub-states expect to target ui-views related@stateName & modal@stateName + // Also wraps mess of generated HTML in a .Panel + wrapPanel(html){ + return `
+ ${html} +
+
+
`; }, buildHTML: function(form, options) { @@ -373,13 +183,13 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat return this.build(options); }, - applyDefaults: function () { - for (var fld in this.form.fields) { - if (this.form.fields[fld]['default'] || this.form.fields[fld]['default'] === 0) { - if (this.form.fields[fld].type === 'select' && this.scope[fld + '_options']) { - this.scope[fld] = this.scope[fld + '_options'][this.form.fields[fld]['default']]; + applyDefaults: function (form, scope) { + for (var fld in form.fields) { + if (form.fields[fld]['default'] || form.fields[fld]['default'] === 0) { + if (form.fields[fld].type === 'select' && scope[fld + '_options']) { + scope[fld] = scope[fld + '_options'][form.fields[fld]['default']]; } else { - this.scope[fld] = this.form.fields[fld]['default']; + scope[fld] = form.fields[fld]['default']; } } } @@ -477,89 +287,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } }, - addListeners: function () { - - if (this.modal) { - $('.jqui-accordion-modal').accordion({ - collapsible: false, - heightStyle: 'content', - active: 0 - }); - } else { - // For help collapse, toggle the plus/minus icon - this.scope.accordionToggle = function (selector) { - $(selector).collapse('toggle'); - if ($(selector + '-icon').hasClass('fa-minus')) { - $(selector + '-icon').removeClass('fa-minus').addClass('fa-plus'); - } else { - $(selector + '-icon').removeClass('fa-plus').addClass('fa-minus'); - } - }; - - $('.jqui-accordion').each(function () { - var active = false, - list = Store('accordions'), - found = false, - id, base, i; - - if ($(this).attr('data-open-first')) { - active = 0; - } - else { - if (list) { - id = $(this).attr('id'); - base = ($location.path().replace(/^\//, '').split('/')[0]); - for (i = 0; i < list.length && found === false; i++) { - if (list[i].base === base && list[i].id === id) { - found = true; - active = list[i].active; - } - } - } - if (found === false && $(this).attr('data-open') === 'true') { - active = 0; - } - } - - $(this).accordion({ - collapsible: true, - heightStyle: 'content', - active: active, - activate: function () { - // Maintain in local storage of list of all accordions by page, recording - // the active panel for each. If user navigates away and comes back, - // we can activate the last panely viewed. - $('.jqui-accordion').each(function () { - var active = $(this).accordion('option', 'active'), - id = $(this).attr('id'), - base = ($location.path().replace(/^\//, '').split('/')[0]), - list = Store('accordions'), - found = false, - i; - if (!list) { - list = []; - } - for (i = 0; i < list.length && found === false; i++) { - if (list[i].base === base && list[i].id === id) { - found = true; - list[i].active = active; - } - } - if (found === false) { - list.push({ - base: base, - id: id, - active: active - }); - } - Store('accordions', list); - }); - } - }); - }); - } - }, - genID: function () { var id = new Date(); return id.getTime(); @@ -632,7 +359,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat buildField: function (fld, field, options, form) { - var i, fldWidth, offset, html = '', + var i, fldWidth, offset, html = '', error_message, horizontal = (this.form.horizontal) ? true : false; function getFieldWidth() { @@ -719,13 +446,14 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (field.label || field.labelBind) { html += "
`; } if (field.awPassMatch) { - var error_message = i18n._("This value does not match the password you entered previously. Please confirm that password."); + error_message = i18n._('This value does not match the password you entered previously. Please confirm that password.'); html += "
" + - error_message + "
\n"; + `.$error.awpassmatch">${error_message}
`; } if (field.awValidUrl) { - var error_message = i18n._("Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."); + error_message = i18n._("Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."); html += "
" + - error_message + "
\n"; + `.$error.awvalidurl">${error_message}
`; } html += "
\n"; @@ -914,6 +637,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\t" + label(); if (field.hasShowInputButton) { + var tooltip = i18n._("Toggle the display of plaintext."); field.toggleInput = function(id) { var buttonId = id + "_show_input_button", inputId = id + "_input", @@ -926,7 +650,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat $(inputId).attr("type", "password"); } }; - var tooltip = i18n._("Toggle the display of plaintext."); html += "\
\n"; @@ -934,7 +657,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\n"; html += "
\n"; } if (field.type === "email") { - var error_message = i18n._("Please enter a valid email address."); + error_message = i18n._("Please enter a valid email address."); html += "
\n" + - error_message + "\n
\n"; + this.form.name + "_form." + fld + `.$error.email'>${error_message}
`; } if (field.awPassMatch) { - var error_message = i18n._("This value does not match the password you entered previously. Please confirm that password."); + error_message = error_message = i18n._("This value does not match the password you entered previously. Please confirm that password."); html += "
\n" + - error_message + "\n
\n"; + `.$error.awpassmatch'>${error_message}
`; } if (field.awValidUrl) { - var error_message = i18n._("Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."); + error_message = i18n._("Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."); html += "
\n" + - error_message + "\n
\n"; + `.$error.awvalidurl'>${error_message}
`; } if (field.chkPass && $AnsibleConfig) { // password strength if ($AnsibleConfig.password_length) { - var error_message = i18n.format(i18n._("Your password must be %d characters long."), $AnsibleConfig.password_length); + error_message = i18n.format(i18n._("Your password must be %d characters long."), $AnsibleConfig.password_length); html += "
" + - error_message + "
\n"; + `.$error.password_length">${error_message}
`; } if ($AnsibleConfig.password_hasLowercase) { - var error_message = i18n._("Your password must contain a lowercase letter."); + error_message = i18n._("Your password must contain a lowercase letter."); html += "
" + - error_message + "
\n"; + `.$error.hasLowercase">${error_message}
`; } if ($AnsibleConfig.password_hasUppercase) { - var error_message = i18n._("Your password must contain an uppercase letter."); + error_message = i18n._("Your password must contain an uppercase letter."); html += "
" + - error_message + "
\n"; + `.$error.hasUppercase">${error_message}
`; } if ($AnsibleConfig.password_hasNumber) { - var error_message = i18n._("Your password must contain a number."); + error_message = i18n._("Your password must contain a number."); html += "
" + - error_message + "
\n"; + `.$error.hasNumber">${error_message}
`; } if ($AnsibleConfig.password_hasSymbol) { - var error_message = i18n.format(i18n._("Your password must contain one of the following characters: %s"), "`~!@#$%^&*()_-+=|}\]{\[;:\"\'?\/>.<,"); + i18n.format(i18n._("Your password must contain one of the following characters: %s"), "`~!@#$%^&*()_-+=|}\]{\[;:\"\'?\/>.<,"); html += "
" + - error_message + "
\n"; + `.$error.hasSymbol">${error_message}
`; } } @@ -1089,8 +802,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += buildId(field, fld, this.form); html += (field.placeholder) ? this.attr(field, 'placeholder') : ""; html += (field.ngDisabled) ? this.attr(field, 'ngDisabled'): ""; - html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; - html += (options.mode === 'add' && field.addRequired) ? "required " : ""; + html += (field.required) ? "required " : ""; html += (field.ngRequired) ? "ng-required=\"" + field.ngRequired +"\"" : ""; html += (field.readonly || field.showonly) ? "readonly " : ""; html += (field.awDropFile) ? "aw-drop-file " : ""; @@ -1119,7 +831,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } // Add error messages - if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired)) { + if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please enter a value.") + "
\n"; } @@ -1148,8 +860,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += (field.ngRequired) ? this.attr(field, 'ngRequired') : ""; html += (field.ngInit) ? this.attr(field, 'ngInit') : ""; html += buildId(field, fld, this.form); - html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; - html += (options.mode === 'add' && field.addRequired) ? "required " : ""; + html += (field.required) ? "required " : ""; //used for select2 combo boxes html += (field.multiSelect) ? "multiple " : ""; html += (field.readonly) ? "disabled " : ""; @@ -1162,13 +873,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if(!field.multiSelect && !field.disableChooseOption){ html += "\n"; } @@ -1193,8 +898,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } // Add error messages - if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired) || - field.awRequiredWhen) { + if (field.required || field.awRequiredWhen) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select a value."); if (field.includePlaybookNotFoundError) { @@ -1237,8 +941,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += (field.ngChange) ? this.attr(field, 'ngChange') : ""; html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : ""; html += (field.slider) ? "id=\"" + fld + "-number\"" : (field.id) ? this.attr(field, 'id') : ""; - html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; - html += (options.mode === 'add' && field.addRequired) ? "required " : ""; + html += (field.required) ? "required " : ""; html += (field.readonly) ? "readonly " : ""; html += (field.integer) ? "integer " : ""; html += (field.disabled) ? "data-disabled=\"true\" " : ""; @@ -1264,7 +967,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } // Add error messages - if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired)) { + if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select a value.") + "
\n"; } @@ -1297,7 +1000,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += buildCheckbox(this.form, field.fields[i], field.fields[i].name, i); } // Add error messages - if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired)) { + if (field.required) { html += "
" + (field.requiredErrorMsg ? field.requiredErrorMsg : "Please select at least one value.") + "
\n"; } @@ -1365,13 +1068,12 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "ng-model=\"" + fld + "\" "; html += (field.ngChange) ? this.attr(field, 'ngChange') : ""; html += (field.readonly) ? "disabled " : ""; - html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; - html += (options.mode === 'add' && field.addRequired) ? "required " : ""; + html += (field.required) ? "required " : ""; html += (field.ngDisabled) ? this.attr(field, 'ngDisabled') : ""; html += " > " + field.options[i].label + "\n"; html += "\n"; } - if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired)) { + if (field.required) { html += "
Please select a value.
\n"; @@ -1420,19 +1122,19 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat //lookup type fields if (field.type === 'lookup') { - + let defaultLookupNgClick = `$state.go($state.current.name + '.${field.sourceModel}')`; html += label(); html += "
\n"; + html += `
`; html += "\n"; - html += "\n"; + html += ``; html += "\n"; html += ""; html += (options.mode === 'edit') ? this.form.editTitle : this.form.addTitle; if(this.form.name === "user"){ - var user_str = i18n._("Admin"); html+= "" + - user_str + ""; - user_str = i18n._("Auditor"); + "ng-show='is_superuser'>Admin"; html+= "" + - user_str + ""; + "ng-show='is_system_auditor'>Auditor"; html+= "LDAP"; html+= ""; html += "
\n"; } - html += "
\n"; //end of Form-header + html += "
"; //end of Form-header } if (!_.isEmpty(this.form.related)) { - var collection; - // i18n is used with src/forms/Projects.js - var details = i18n._("Details"); - html += "
"; + var collection, details = i18n._('Details'); + html += `
`; if(this.mode === "edit"){ - html += "
" + - details + "
"; + html += `
` + + `${details}
`; for (itm in this.form.related) { collection = this.form.related[itm]; - html += `
" + - details + "
"; + `class="Form-tab is-selected">${details}
`; for (itm in this.form.related) { collection = this.form.related[itm]; @@ -1603,8 +1300,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } if(!_.isEmpty(this.form.related) && this.mode === "edit"){ - html += "
"; + html += `
`; } html += "\n"; html += "
{{ flashMessage }}
\n"; - if (this.form.licenseTabs) { - html += "
    \n"; - for (i = 0; i < this.form.licenseTabs.length; i++) { - tab = this.form.licenseTabs[i]; - html += "" + tab.label + "\n"; - } - html += "
\n"; - html += "
\n"; - for (i = 0; i < this.form.licenseTabs.length; i++) { - tab = this.form.licenseTabs[i]; - html += "
\n"; - for (fld in this.form.fields) { - if (this.form.fields[fld].tab === tab.name) { - html += this.buildField(fld, this.form.fields[fld], options, this.form); - } - } - html += "
\n"; - } - html += "
\n"; - } else { var currentSubForm; var hasSubFormField; // original, single-column form @@ -1690,6 +1357,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } html += this.buildField(fld, field, options, this.form); + // console.log('*********') + // console.log(html) } } @@ -1703,7 +1372,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if (group !== '') { html += "
\n"; } - } html += "\n"; @@ -1714,7 +1382,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "
"; - html += this.GenerateCollection({ form: this.form, related: itm }, options); - html += "
\n"; - } - } - } } - - return html; + if (this.form.include){ + _.forEach(this.form.include, (template) =>{ + html += `
`; + }); + } + // console.log(html) + return this.wrapPanel(html); }, - buildCollections: function (options) { - // - // Create TB accordians with imbedded lists for related collections - // Should not be called directly. Called internally by build(). - // - var form = this.form, - html = '', - itm, collection; + buildCollection: function (params) { + // Currently, there are two ways we reference a list definition in a form + // Permissions lists are defined with boilerplate JSON in model.related + // this.GenerateCollection() is shaped around supporting this definition + // Notifications lists contain a reference to the NotificationList object, which contains the list's JSON definition + // However, Notification Lists contain fields that are only rendered by with generateList.build's chain + // @extendme rip out remaining HTML-concat silliness and directivize ¯\_(ツ)_/¯ + this.form = params.form; + var html = '', + collection = this.form.related[params.related]; - if (!options.collapseAlreadyStarted) { - html = "
\n"; - } - - for (itm in form.related) { - collection = form.related[itm]; - html += "

" + (collection.title || collection.editTitle) + "

\n"; - html += "
\n"; if (collection.generateList) { - html += GenerateList.buildHTML(collection, { mode: 'edit' }); + html += GenerateList.build({ mode: params.mode, list: collection}); } else { - html += this.GenerateCollection({ form: form, related: itm }, options); + html += this.GenerateCollection({ form: this.form, related: params.related }, {mode: params.mode}); } - html += "
\n"; // accordion inner - } - - if (!options.collapseAlreadyStarted) { - html += "
\n"; // accordion body - } return html; }, @@ -1869,13 +1517,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "Hint: " + collection.instructions + "\n"; html += "
\n"; } - var rootID = $location.$$path.split("/")[2]; - var endpoint = (collection.basePath) ? "/api/v1/" + - collection.basePath - .replace(":id", rootID) : ""; - var tagSearch = getSearchHtml - .inject(getSearchHtml.getList(collection), - endpoint, itm, collection.iterator); var actionButtons = ""; Object.keys(collection.actions || {}) @@ -1884,7 +1525,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat .actions[act]); }); var hideOnSuperuser = (hideOnSuperuser === true) ? true : false; - if(actionButtons.length === 0 ){ // The search bar should be full width if there are no // action buttons @@ -1893,38 +1533,37 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat else { width = "col-lg-8 col-md-8 col-sm-8 col-xs-12"; } + + // smart-search directive html += ` -
-
0) && - !(is_superuser && ${hideOnSuperuser})\"> - ${tagSearch} -
`; +
+ + +
+ `; if(actionButtons.length>0){ - html += `
-
+ html += `
${actionButtons} -
-
`; +
`; } - html += "
"; + //html += "
"; // Message for when a search returns no results. This should only get shown after a search is executed with no results. - html += `
-
+ class="row" + ng-show="${itm}.length === 0 && !(${collection.iterator}_searchTags | isEmpty)"> +
No records matched your search.
@@ -1932,61 +1571,35 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Show the "no items" box when loading is done and the user isn't actively searching and there are no results var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; - html += '
'; - html += "
" + emptyListText + "
"; + html += `
`; + html += `
${emptyListText}
`; html += '
'; html += ` -
+
System Administrators have access to all ${collection.iterator}s
`; // Start the list html += ` -
0)) && - !(is_superuser && ${collection.hideOnSuperuser})\"> - +
+
- + `; html += (collection.index === undefined || collection.index !== false) ? "\n" : ""; for (fld in collection.fields) { - if (!collection.fields[fld].searchOnly) { - - html += "\n"; - } + html += ``; } if (collection.fieldActions) { html += "\n"; @@ -2063,12 +1676,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat //html += "\n"; // close well html += "\n"; // close list-wrapper div - html += PaginateWidget({ - set: itm, - iterator: collection.iterator, - mini: true, - hideOnSuperuser: collection.hideOnSuperuser - }); + html += ``; return html; } }; diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index 6d6b4b70fc..e1697ca25c 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -676,175 +676,13 @@ angular.module('GeneratorHelpers', [systemStatus.name]) }; }) -.factory('SearchWidget', function () { - return function (params) { - // - // Generate search widget - // - var iterator = params.iterator, - form = params.template, - size = params.size, - mini = params.mini, - includeSize = (params.includeSize === undefined) ? true : params.includeSize, - ngShow = (params.ngShow) ? params.ngShow : false, - i, html = '', - modifier, - searchWidgets = (params.searchWidgets) ? params.searchWidgets : 1, - sortedKeys; - - function addSearchFields(idx) { - var html = ''; - sortedKeys = Object.keys(form.fields).sort(); - sortedKeys.forEach(function(fld) { - if ((form.fields[fld].searchable === undefined || form.fields[fld].searchable === true) && - (((form.fields[fld].searchWidget === undefined || form.fields[fld].searchWidget === 1) && idx === 1) || - (form.fields[fld].searchWidget === idx))) { - html += "
  • " + - form.fields[fld].searchLabel + "
  • \n"; - } else { - html += form.fields[fld].label.replace(/
    /g, ' ') + "', " + idx + ")\">" + - form.fields[fld].label.replace(/
    /g, ' ') + "\n"; - } - } - }); - return html; - } - - for (i = 1; i <= searchWidgets; i++) { - modifier = (i === 1) ? '' : i; - - if (includeSize) { - html += "
    \n"; - } - - if(ngShow) { - html += "
    "; - } - - html += "
    \n"; - html += "
    \n"; - html += "\n"; - html += "
      \n"; - html += addSearchFields(i); - html += "
    \n"; - html += "
    \n"; - - html += "\n"; - - html += "\n"; - - // Reset button for drop-down - html += "
    \n"; - html += "\n"; - html += "
    \n"; - - html += "
    \n"; - - html += "\n"; - - html += "\n"; - - html += "
    \n"; - - if(ngShow) { - html += "
    "; - } - - if (includeSize) { - html += "
    \n"; - } - } - - return html; - - }; -}) - -.factory('PaginateWidget', [ - function () { - return function (params) { - var iterator = params.iterator, - set = params.set, - hideOnSuperuser = (params.hideOnSuperuser) ? true : false, - html = ''; - html += "\n"; - html += ` -
    - `; - html += "
    "; - html += "
    "; - html += "
      \n"; - html += "
    • " + - "
    • \n"; - - html += "
    • " + - "
    • \n"; - - // html += "
    • " + - // "{{ page }}
    • \n"; - html += "
    • " + - "{{ page }}
    • \n"; - - html += "
    • " + iterator + "_num_pages\">
    • \n"; - - html += "
    • = " + iterator + "_num_pages\">
    • \n"; - html += "
    \n"; - html += "Page {{ " + iterator + "_page }} of {{ " + iterator + "_num_pages }}"; - html += "
    "; - html += "
    "; - html += "
    \n"; - html += "ITEMS "; - html += "{{ (" + iterator + "_total_rows | number:0) < 1 ? 0 : (" + iterator + "_page-1)*" + iterator + "_page_size+1}}"; - html += "–{{ (" + iterator + "_total_rows | number:0) < (" + iterator + "_page)*" + iterator + "_page_size ? (" + iterator + "_total_rows | number:0) : (" + iterator + "_page)*" + iterator + "_page_size}}"; - html += " OF "; - html += "{{ " + iterator + "_total_rows | number:0 }}"; - html += ""; - html += "
    \n"; - html += "
    \n"; - - return html; - }; - } -]) .factory('ActionButton', function () { return function (options) { var html = ''; + console.log(options) + html += '\n"; - } - html += "
    "; - html += "
    "; - html += "
    \n"; - - for (action in list.actions) { - list.actions[action] = _.defaults(list.actions[action], { dataPlacement: "top" }); - } - - html += "
    "; - if(list.toolbarAuxAction) { - html += "
    "; - html += list.toolbarAuxAction; - html += "
    "; - } - html += "\n
    "; - html += "
    "; - html += ""; - } } + html += ""; + if (options.cancelButton === true) { + html += "
    "; + html += "
    \n"; + } + html += "
    "; + html += "
    "; + html += `
    `; - if (options.mode === 'edit' && list.editInstructions) { - html += "
    \n"; - html += "\n"; - html += "Hint: " + list.editInstructions + "\n"; - html += "
    \n"; - } + for (action in list.actions) { + list.actions[action] = _.defaults(list.actions[action], { dataPlacement: "top" }); + } - if (options.instructions) { - html += "
    " + options.instructions + "
    \n"; - } - else if (list.instructions) { - html += "
    " + list.instructions + "
    \n"; + html += "
    "; + if (list.toolbarAuxAction) { + html += "
    "; + html += list.toolbarAuxAction; + html += "
    "; + } + html += "\n
    "; + html += "
    "; + html += ""; } + } - if (options.mode !== 'lookup' && (list.well === undefined || list.well)) { - html += "
    \n"; - } - html += (list.searchRowActions) ? "
    " : ""; - if (list.searchRowActions && !list.searchSize) { - list.searchSize = 'col-lg-7 col-md-12 col-sm-12 col-xs-12'; - } - if (options.showSearch=== undefined || options.showSearch === true) { - var tagSearch = getSearchHtml - .inject(getSearchHtml.getList(list), - getSearchHtml.getEndpoint(list), - list.name, - list.iterator); - html += ` -
    - ${tagSearch} -
    + if (options.mode === 'edit' && list.editInstructions) { + html += "
    \n"; + html += "\n"; + html += "Hint: " + list.editInstructions + "\n"; + html += "
    \n"; + } + + if (options.instructions) { + html += "
    " + options.instructions + "
    \n"; + } else if (list.instructions) { + html += "
    " + list.instructions + "
    \n"; + } + + if (options.mode !== 'lookup' && (list.well === undefined || list.well)) { + html += `
    `; + } + + html += (list.searchRowActions) ? "
    " : ""; + if (list.searchRowActions && !list.searchSize) { + list.searchSize = 'col-lg-7 col-md-12 col-sm-12 col-xs-12'; + } + if (options.showSearch === undefined || options.showSearch === true) { + html += ` +
    + + +
    `; - } - if(list.searchRowActions) { - html += "
    "; + } + if (list.searchRowActions) { + html += "
    "; - var actionButtons = ""; - Object.keys(list.searchRowActions || {}) - .forEach(act => { - actionButtons += ActionButton(list.searchRowActions[act]); - }); - html += ` + var actionButtons = ""; + Object.keys(list.searchRowActions || {}) + .forEach(act => { + actionButtons += ActionButton(list.searchRowActions[act]); + }); + html += `
    ${actionButtons}
    `; - html += "
    "; + html += "
    "; + } + + if (options.showSearch !== false) { + // Message for when a search returns no results. This should only get shown after a search is executed with no results. + html +=` +
    +
    No records matched your search.
    +
    + `; + } + + // 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 : i18n._("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 += "
    "; + html += (options.mode === 'edit' || options.mode === 'summary') ? list.editTitle : list.addTitle; + html += "
    \n"; + } + + // table header row + html += "
    0\""; + html += (list.awCustomScroll) ? " aw-custom-scroll " : ""; + html += ">\n"; + + function buildTable() { + var extraClasses = list['class']; + var multiSelect = list.multiSelect ? 'multi-select-list' : null; + var multiSelectExtended = list.multiSelectExtended ? 'true' : 'false'; + + if (options.mode === 'summary') { + extraClasses += ' table-summary'; } - 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"; + return $('
    #"; - } else { - html += ">"; - } - - - html += collection.fields[fld].label; - - if (!(collection.fields[fld].noSort || collection.fields[fld].nosort)) { - html += " "; - } - - html += " + ${collection.fields[fld].label} + Actions
    ') + .attr('id', list.name + '_table') + .addClass('List-table') + .addClass(extraClasses) + .attr('multi-select-list', multiSelect) + .attr('is-extended', multiSelectExtended); + + } + + var table = buildTable(); + var innerTable = ''; + + if (!options.skipTableHead) { + innerTable += this.buildHeader(options); + } + + // table body + // gotcha: transcluded elements require custom scope linking - binding to $parent models assumes a very rigid DOM hierarchy + // see: lookup-modal.directive.js for example + innerTable += options.mode === 'lookup' ? `` : `"\n"`; + innerTable += "\n"; + + if (list.index) { + innerTable += "\n"; + } + + if (list.multiSelect) { + innerTable += ''; + } + + // Change layout if a lookup list, place radio buttons before labels + if (options.mode === 'lookup') { + if (options.input_type === "radio") { //added by JT so that lookup forms can be either radio inputs or check box inputs + innerTable += ``; + } else { // its assumed that options.input_type = checkbox + innerTable += ""; } + } - // 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 : i18n._("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 += "
    "; - html += (options.mode === 'edit' || options.mode === 'summary') ? list.editTitle : list.addTitle; - html += "
    \n"; + cnt = 2; + base = (list.base) ? list.base : list.name; + base = base.replace(/^\//, ''); + for (fld in list.fields) { + cnt++; + if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && + !(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) { + innerTable += Column({ + list: list, + fld: fld, + options: options, + base: base + }); } + } - // table header row - html += "
    0)\""; - html += (list.awCustomScroll) ? " aw-custom-scroll " : ""; - html += ">\n"; - - function buildTable() { - var extraClasses = list['class']; - var multiSelect = list.multiSelect ? 'multi-select-list' : null; - var multiSelectExtended = list.multiSelectExtended ? 'true' : 'false'; - - if (options.mode === 'summary') { - extraClasses += ' table-summary'; - } - - return $('
    {{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.
    ') - .attr('id', list.name + '_table') - .addClass('List-table') - .addClass(extraClasses) - .attr('multi-select-list', multiSelect) - .attr('is-extended', multiSelectExtended); - - } - - var table = buildTable(); - var innerTable = ''; - - if (!options.skipTableHead) { - innerTable += this.buildHeader(options); - } - - // table body - innerTable += "\n"; - innerTable += "\n"; - - if (list.index) { - innerTable += "\n"; - } - - if (list.multiSelect) { - innerTable += ''; - } - - // Change layout if a lookup list, place radio buttons before labels - if (options.mode === 'lookup') { - if(options.input_type==="radio"){ //added by JT so that lookup forms can be either radio inputs or check box inputs - innerTable += ""; - } - else { // its assumed that options.input_type = checkbox - innerTable += ""; - } - } - - cnt = 2; - base = (list.base) ? list.base : list.name; - base = base.replace(/^\//, ''); - for (fld in list.fields) { - cnt++; - if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && - !(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) { - innerTable += Column({ - list: list, - fld: fld, - options: options, - base: base - }); - } - } - - if (options.mode === 'select') { - if(options.input_type==="radio"){ //added by JT so that lookup forms can be either radio inputs or check box inputs - innerTable += ""; - } - else { // its assumed that options.input_type = checkbox - innerTable += ""; - } - } else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) { + } + } else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) { - // Row level actions + // Row level actions - innerTable += "\n"; } - - innerTable += "\n"; - - // Message for loading - innerTable += "\n"; - var loading = i18n._("Loading..."); - innerTable += "\n"; - innerTable += "\n"; - - // End List - innerTable += "\n"; - - table.html(innerTable); - html += table.prop('outerHTML'); - - html += "\n"; - - if (options.mode === 'select' && (options.selectButton === undefined || options.selectButton)) { - html += "
    \n"; - html += " \n"; - html += "
    \n"; - } - - if (options.mode !== 'lookup' && (list.well === undefined || list.well === true)) { - html += "\n"; //well - } - - if (options.mode === 'lookup' || (options.id && options.id === "form-modal-body")) { - html += PaginateWidget({ - set: list.name, - iterator: list.iterator - }); - } else { - html += PaginateWidget({ - set: list.name, - iterator: list.iterator - }); - } - - return html; - }, - - buildHeader: function(options) { - var list = this.list, - fld, html; - - function buildSelectAll() { - return $('\n"; - html += "\n"; - if (list.index) { - html += "\n"; - } - - if (list.multiSelect) { - html += buildSelectAll().prop('outerHTML'); - } - else if (options.mode === 'lookup') { - html += ""; - } - for (fld in list.fields) { - if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && - !(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) { - html += "\n"; - } - } - if (options.mode === 'select') { - html += ""; - } else if (options.mode === 'edit' && list.fieldActions) { - html += "\n"; - } - html += "\n"; - html += "\n"; - return html; + innerTable += "\n"; } - }; - }]; + + innerTable += "\n"; + + // End List + innerTable += "\n"; + + table.html(innerTable); + html += table.prop('outerHTML'); + + html += "\n"; + + if (options.mode === 'select' && (options.selectButton === undefined || options.selectButton)) { + html += "
    \n"; + html += " \n"; + html += "
    \n"; + } + + html += ` + `; + + return html; + }, + + buildHeader: function(options) { + var list = this.list, + fld, html; + + function buildSelectAll() { + return $('\n"; + html += "\n"; + if (list.index) { + html += "\n"; + } + + if (list.multiSelect) { + html += buildSelectAll().prop('outerHTML'); + } else if (options.mode === 'lookup') { + html += ""; + } + + if (options.mode !== 'lookup'){ + for (fld in list.fields) { + let customClass = list.fields[fld].columnClass || ''; + html += ``; + } + } + if (options.mode === 'lookup') { + let customClass = list.fields.name.modalColumnClass || ''; + html += ``; + + } + if (options.mode === 'select') { + html += ""; + } else if (options.mode === 'edit' && list.fieldActions) { + html += "\n"; + } + html += "\n"; + html += "\n"; + return html; + }, + + wrapPanel: function(html){ + return`
    ${html}
    `; + } + }; + } +]; diff --git a/awx/ui/client/src/shared/lookup/lookup-modal.block.less b/awx/ui/client/src/shared/lookup/lookup-modal.block.less new file mode 100644 index 0000000000..a0f331ca0f --- /dev/null +++ b/awx/ui/client/src/shared/lookup/lookup-modal.block.less @@ -0,0 +1,6 @@ +.Lookup .modal-body{ + padding-top: 0px; +} +.Lookup-cancel{ + margin-right: 20px; +} diff --git a/awx/ui/client/src/shared/lookup/lookup-modal.directive.js b/awx/ui/client/src/shared/lookup/lookup-modal.directive.js new file mode 100644 index 0000000000..5950363d3c --- /dev/null +++ b/awx/ui/client/src/shared/lookup/lookup-modal.directive.js @@ -0,0 +1,34 @@ +export default ['templateUrl', '$compile', function(templateUrl, $compile) { + return { + restrict: 'E', + replace: true, + transclude: true, + scope: false, + templateUrl: templateUrl('shared/lookup/lookup-modal'), + link: function(scope, element, attrs, controller, transcludefn) { + + transcludefn(scope, (clone, linked_scope) => { + // scope.$resolve is a reference to resolvables in stateDefinition.resolve block + // https://ui-router.github.io/docs/latest/interfaces/state.statedeclaration.html#resolve + let list = linked_scope.$resolve.ListDefinition, + Dataset = linked_scope.$resolve.Dataset; + // search init + linked_scope.list = list; + linked_scope[`${list.iterator}_dataset`] = Dataset.data; + linked_scope[list.name] = linked_scope[`${list.iterator}_dataset`].results; + + element.find('.modal-body').append(clone); + + }); + $('#form-modal').modal('show'); + }, + controller: ['$scope', '$state', function($scope, $state) { + $scope.saveForm = function() { + let list = $scope.list; + $scope.$parent[`${list.iterator}_name`] = $scope.selection[list.iterator].name; + $scope.$parent[list.iterator] = $scope.selection[list.iterator].id; + $state.go('^'); + }; + }] + }; +}]; diff --git a/awx/ui/client/src/shared/lookup/lookup-modal.partial.html b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html new file mode 100644 index 0000000000..7840d25217 --- /dev/null +++ b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html @@ -0,0 +1,24 @@ + diff --git a/awx/ui/client/src/shared/lookup/main.js b/awx/ui/client/src/shared/lookup/main.js new file mode 100644 index 0000000000..df069649ab --- /dev/null +++ b/awx/ui/client/src/shared/lookup/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import directive from './lookup-modal.directive'; + +export default + angular.module('LookupModalModule', []) + .directive('lookupModal', directive); diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 8f92fab287..78bc5d19f9 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -5,22 +5,40 @@ *************************************************/ import listGenerator from './list-generator/main'; -import pagination from './pagination/main'; +import formGenerator from './form-generator'; +import lookupModal from './lookup/main'; +import smartSearch from './smart-search/main'; +import paginate from './paginate/main'; +import columnSort from './column-sort/main'; +import title from './title.directive'; import lodashAsPromised from './lodash-as-promised'; import stringFilters from './string-filters/main'; import truncatedText from './truncated-text.directive'; import stateExtender from './stateExtender.provider'; import rbacUiControl from './rbacUiControl'; import socket from './socket/main'; +import templateUrl from './template-url/main'; +import RestServices from '../rest/main'; +import stateDefinitions from './stateDefinitions.factory'; +import apiLoader from './api-loader'; export default angular.module('shared', [listGenerator.name, - pagination.name, + formGenerator.name, + lookupModal.name, + smartSearch.name, + paginate.name, + columnSort.name, stringFilters.name, 'ui.router', rbacUiControl.name, - socket.name + socket.name, + templateUrl.name, + RestServices.name, + apiLoader.name, + require('angular-cookies'), ]) + .factory('stateDefinitions', stateDefinitions) .factory('lodashAsPromised', lodashAsPromised) .directive('truncatedText', truncatedText) .provider('$stateExtender', stateExtender); diff --git a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js index 4867c5e07c..068abb1fa7 100644 --- a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js +++ b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js @@ -30,7 +30,7 @@ export default item: '=item' }, require: '^multiSelectList', - template: '', + template: '', link: function(scope, element, attrs, multiSelectList) { scope.decoratedItem = multiSelectList.registerItem(scope.item); diff --git a/awx/ui/client/src/shared/paginate/main.js b/awx/ui/client/src/shared/paginate/main.js new file mode 100644 index 0000000000..394f9b9034 --- /dev/null +++ b/awx/ui/client/src/shared/paginate/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import directive from './paginate.directive'; +import controller from './paginate.controller'; + +export default + angular.module('PaginateModule', []) + .directive('paginate', directive) + .controller('PaginateController', controller); diff --git a/awx/ui/client/src/shared/paginate/paginate.block.less b/awx/ui/client/src/shared/paginate/paginate.block.less new file mode 100644 index 0000000000..c7836dae3d --- /dev/null +++ b/awx/ui/client/src/shared/paginate/paginate.block.less @@ -0,0 +1,53 @@ + @import "./client/src/shared/branding/colors.default.less"; + @import "./client/src/shared/branding/colors.less"; + // @todo cleanup these messy overrides for styles in ansible-ui.min.css + + +.Paginate-controls--first a, +.Paginate-controls--previous a{ + border-radius: 4px 0 0 4px; + } +.Paginate-controls--last a, +.Paginate-controls--next a{ + border-radius: 0px 4px 4px 0; +} + + .Paginate-controls--item a { + font-size: 12px; + padding: 3px 6px !important; + border-color: @list-pagin-bord; + } + + .Paginate { + margin-top: 20px; + font-size: 12px !important; + color: @list-pagin-text; + text-transform: uppercase; + height: 22px; + display: flex; + justify-content: flex-end; + } + + .Paginate-pager--pageof { + line-height: 22px; + margin-left: 10px; + } + + .Paginate-wrapper { + display: flex; + flex: 1 0 auto; + } + + .Paginate-controls { + margin-top: 0; + margin-bottom: 7px; + display: inline-block; + padding-left: 0; + border-radius: 4px; + } + + .Paginate-controls--active { + color: #fff !important; + border-color: @default-icon-hov !important; + background-color: @default-icon-hov !important; + } diff --git a/awx/ui/client/src/shared/paginate/paginate.controller.js b/awx/ui/client/src/shared/paginate/paginate.controller.js new file mode 100644 index 0000000000..b8d0a4ecf5 --- /dev/null +++ b/awx/ui/client/src/shared/paginate/paginate.controller.js @@ -0,0 +1,66 @@ +export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'QuerySet', + function($scope, $stateParams, $state, $filter, GetBasePath, qs) { + + let pageSize = $stateParams[`${$scope.iterator}_search`].page_size || 20, + queryset, path; + $scope.pageSize = pageSize; + + function init() { + $scope.pageRange = calcPageRange($scope.current(), $scope.last()); + $scope.dataRange = calcDataRange(); + } + $scope.dataCount = function() { + return $filter('number')($scope.dataset.count); + }; + + $scope.toPage = function(page) { + path = GetBasePath($scope.basePath) || $scope.basePath; + queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page }); + $state.go('.', { + [$scope.iterator + '_search']: queryset + }); + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); + $scope.pageRange = calcPageRange($scope.current(), $scope.last()); + $scope.dataRange = calcDataRange(); + }; + + $scope.current = function() { + return parseInt($stateParams[`${$scope.iterator}_search`].page || '1'); + }; + + $scope.last = function() { + return Math.ceil($scope.dataset.count / pageSize); + }; + + function calcPageRange(current, last) { + let result = []; + if (last < 10) { + result = _.range(1, last + 1); + } else if (current - 5 > 0 && current !== last) { + result = _.range(current - 5, current + 6); + } else if (current === last) { + result = _.range(last - 10, last + 1); + } else { + result = _.range(1, 11); + } + return result; + } + + function calcDataRange() { + if ($scope.current() == 1 && $scope.dataset.count < parseInt(pageSize)) { + return `1 - ${$scope.dataset.count}`; + } else if ($scope.current() == 1) { + return `1 - ${pageSize}`; + } else { + let floor = (($scope.current() - 1) * parseInt(pageSize)) + 1; + let ceil = floor + parseInt(pageSize); + return `${floor} - ${ceil}`; + } + } + + init(); + } +]; diff --git a/awx/ui/client/src/shared/paginate/paginate.directive.js b/awx/ui/client/src/shared/paginate/paginate.directive.js new file mode 100644 index 0000000000..e839c5b6b0 --- /dev/null +++ b/awx/ui/client/src/shared/paginate/paginate.directive.js @@ -0,0 +1,16 @@ +export default ['templateUrl', + function(templateUrl) { + return { + restrict: 'E', + replace: false, + scope: { + collection: '=', + dataset: '=', + iterator: '@', + basePath: '@' + }, + controller: 'PaginateController', + templateUrl: templateUrl('shared/paginate/paginate') + }; + } +]; diff --git a/awx/ui/client/src/shared/paginate/paginate.partial.html b/awx/ui/client/src/shared/paginate/paginate.partial.html new file mode 100644 index 0000000000..ac1eff1d35 --- /dev/null +++ b/awx/ui/client/src/shared/paginate/paginate.partial.html @@ -0,0 +1,44 @@ +
    +
    + + Page + {{current()}} of + {{last()}} + +
    +
    + ITEMS  + {{dataRange}} + of {{dataCount()}} + +
    +
    diff --git a/awx/ui/client/src/shared/pagination/main.js b/awx/ui/client/src/shared/pagination/main.js deleted file mode 100644 index 088bb0e399..0000000000 --- a/awx/ui/client/src/shared/pagination/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import pagination from './pagination.service'; - -export default - angular.module('pagination', []) - .factory('pagination', pagination); diff --git a/awx/ui/client/src/shared/pagination/pagination.service.js b/awx/ui/client/src/shared/pagination/pagination.service.js deleted file mode 100644 index 73249da8ad..0000000000 --- a/awx/ui/client/src/shared/pagination/pagination.service.js +++ /dev/null @@ -1,44 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default ['$http', '$q', function($http, $q) { - return { - getInitialPageForList: function(id, url, pageSize) { - // get the name of the object - if ($.isNumeric(id)) { - return $http.get(url + "?id=" + id) - .then(function (data) { - var queryValue, queryType; - if (data.data.results.length) { - if (data.data.results[0].type === "user") { - queryValue = data.data.results[0].username; - queryType = "username"; - } else { - queryValue = data.data.results[0].name; - queryType = "name"; - } - } else { - queryValue = ""; - queryType = "name"; - } - // get how many results are less than or equal to - // the name - return $http.get(url + "?" + queryType + "__lte=" + queryValue) - .then(function (data) { - // divide by the page size to get what - // page the data should be on - var count = data.data.count; - return Math.max(1, Math.ceil(count/parseInt(pageSize))); - }); - }); - } else { - var defer = $q.defer(); - defer.resolve(1); - return(defer.promise); - } - } - }; -}]; diff --git a/awx/ui/client/src/shared/smart-search/django-search-model.class.js b/awx/ui/client/src/shared/smart-search/django-search-model.class.js new file mode 100644 index 0000000000..5271a38a30 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/django-search-model.class.js @@ -0,0 +1,56 @@ +// Ignored fields are not surfaced in the UI's search key +let isIgnored = function(key, value) { + let ignored = [ + 'type', + 'url', + 'related', + 'summary_fields', + 'object_roles', + 'activity_stream', + 'update', + 'teams', + 'users', + 'owner_teams', + 'owner_users', + 'access_list', + 'notification_templates_error', + 'notification_templates_success', + 'ad_hoc_command_events', + 'fact_versions', + 'variable_data', + 'playbooks' + ]; + return ignored.indexOf(key) > -1 || value.type === 'field'; +}; + +export default +class DjangoSearchModel { + /* + @property name - supplied model name + @property base { + field: { + type: 'string' // string, bool, field, choice, datetime, + label: 'Label', // Capitalized + help_text: 'Some helpful descriptive text' + } + } + @@property related ['field' ...] + */ + constructor(name, endpoint, baseFields, relations) { + let base = {}; + this.name = name; + this.related = _.reject(relations, isIgnored); + _.forEach(baseFields, (value, key) => { + if (!isIgnored(key, value)) { + base[key] = value; + } + }); + this.base = base; + } + + fields() { + let result = this.base; + result.related = this.related; + return result; + } +} diff --git a/awx/ui/client/src/shared/smart-search/main.js b/awx/ui/client/src/shared/smart-search/main.js new file mode 100644 index 0000000000..7653df7bd7 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/main.js @@ -0,0 +1,12 @@ +import directive from './smart-search.directive'; +import controller from './smart-search.controller'; +import service from './queryset.service'; +import DjangoSearchModel from './django-search-model.class'; + + +export default +angular.module('SmartSearchModule', []) + .directive('smartSearch', directive) + .controller('SmartSearchController', controller) + .service('QuerySet', service) + .constant('DjangoSearchModel', DjangoSearchModel); diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js new file mode 100644 index 0000000000..12057c1a90 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -0,0 +1,116 @@ +export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'GetBasePath', + function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, GetBasePath) { + return { + // kick off building a model for a specific endpoint + // this is usually a list's basePath + // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields + initFieldset(path, name, relations) { + // get or set $cachFactory.Cache object with id '$http' + let defer = $q.defer(), + cache = $cacheFactory.get('$http') || $cacheFactory('$http'); + defer.resolve(this.getCommonModelOptions(path, name, relations, cache)); + return defer.promise; + }, + + getCommonModelOptions(path, name, relations, cache) { + let resolve, base, + defer = $q.defer(); + + // grab a single model from the cache, if present + if (cache.get(path)) { + defer.resolve({[name] : new DjangoSearchModel(name, path, cache.get(path), relations)}); + } else { + this.url = path; + resolve = this.options(path) + .then((res) => { + base = res.data.actions.GET; + defer.resolve({[name]: new DjangoSearchModel(name, path, base, relations)}); + }); + } + return defer.promise; + }, + + /* @extendme + // example: + // retrieving options from a polymorphic model (unified_job) + getPolymorphicModelOptions(path, name) { + let defer = $q.defer(), + paths = { + project_update: GetBasePath('project_update'), + inventory_update: GetBasePath('inventory_update'), + job: GetBasePath('jobs'), + ad_hoc_command: GetBasePath('ad_hoc_commands'), + system_job: GetBasePath('system_jobs') + }; + defer.all( // for each getCommonModelOptions() ); + return defer.promise; + }, + */ + + // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL + encodeQueryset(params) { + let queryset; + queryset = _.reduce(params, (result, value, key) => { + return result + `${key}=${value}&`; + }, ''); + queryset = queryset.substring(0, queryset.length - 1); + return angular.isObject(params) ? `?${queryset}` : ''; + }, + // encodes a ui smart-search param to a django-friendly param + // operand:key:comparator:value => {operand__key__comparator: value} + encodeParam(param){ + let split = param.split(':'); + return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]}; + }, + // decodes a django queryset param into ui smart-search param + decodeParam(key, value){ + return `${key.split('__').join(':')}:${value}`; + }, + + // encodes a django queryset for ui-router's URLMatcherFactory + // {operand__key__comparator: value, } => 'operand:key:comparator:value,...' + encodeArr(params) { + let url; + url = _.reduce(params, (result, value, key) => { + return result.concat(`${key}:${value}`); + }, []); + return url.join(';'); + }, + + // decodes a django queryset for ui-router's URLMatcherFactory + // 'operand:key:comparator:value,...' => {operand__key__comparator: value, } + decodeArr(arr) { + let params = {}; + _.forEach(arr.split(';'), (item) => { + let key = item.split(':')[0], + value = item.split(':')[1]; + params[key] = value; + }); + return params; + }, + // REST utilities + options(endpoint) { + Rest.setUrl(endpoint); + return Rest.options(endpoint); + }, + search(endpoint, params) { + Wait('start'); + this.url = `${endpoint}${this.encodeQueryset(params)}`; + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + error(data, status) { + ProcessErrors($rootScope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + this.url + '. GET returned: ' + status + }); + }, + success(data) { + return data; + }, + }; + } +]; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.block.less b/awx/ui/client/src/shared/smart-search/smart-search.block.less new file mode 100644 index 0000000000..23afbc7791 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/smart-search.block.less @@ -0,0 +1,234 @@ +@import "../branding/colors.default.less"; +.SmartSearch { + margin-bottom: 10px; + min-height: 45px; + padding-left: 15px; + padding-right: 15px; +} + +.SmartSearch-form { + width: 100%; +} + +.SmartSearch-bar { + display: flex; + padding: 0; + font-size: 12px; + height: 35px; + align-items: stretch; + margin-bottom: 10px; + line-height: 20px; +} +// `.${list.name}List` class can be used to set add custom class overrides +.groupsList .SmartSearch-bar, .hostsList .SmartSearch-bar, .PortalMode .SmartSearch-bar{ + width: 100%; +} + +.SmartSearch-tags{ + padding-left: 0px; +} + +.SmartSearch-bar i { + font-size: 16px; + color: @default-icon; +} + +.SmartSearch-searchTermContainer { + flex: initial; + width: ~"calc(100% - 100px)"; + border: 1px solid @d7grey; + border-radius: 4px; + display: flex; + background-color: @default-bg; + position: relative; +} + +.SmartSearch-searchTermContainer.is-open { + border-bottom-right-radius: 0; +} + +.SmartSearch-input { + flex: 1 0 auto; + margin: 0 10px; + border: none; + font-size: 14px; + height: 100%; + width: 100%; +} + +.SmartSearch-input:focus, +.SmartSearch-input:active { + outline: 0; +} + +.SmartSearch-searchTermContainer input:placeholder-shown { + color: @default-icon !important; + text-transform: uppercase; +} + +.SmartSearch-searchButton { + flex: initial; + margin-left: auto; + padding: 8px 10px; + border-left: 1px solid @d7grey; + background-color: @default-bg; + cursor: pointer; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.SmartSearch-searchButton:hover { + background-color: @default-tertiary-bg; +} + +.SmartSearch-flexContainer { + display: flex; + width: 100%; + flex-wrap: wrap; +} + +.SmartSearch-tagContainer { + display: flex; + max-width: 100%; + margin-bottom: 10px; +} + +.SmartSearch-tag { + border-radius: 5px; + padding: 2px 10px; + margin: 4px 0px; + font-size: 12px; + color: @default-interface-txt; + background-color: @default-bg; + margin-right: 5px; + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.SmartSearch-tag--deletable { + margin-right: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-right: 0; + max-width: ~"calc(100% - 23px)"; + background-color: @default-link; + color: @default-bg; + margin-right: 5px; +} + +.SmartSearch-deleteContainer { + background-color: @default-link!important; + color: white; + background-color: @default-bg; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + padding: 0 5px; + margin: 4px 0px; + align-items: center; + display: flex; + cursor: pointer; +} + +.SmartSearch-tagDelete { + font-size: 13px; +} + +.SmartSearch-name { + flex: initial; + max-width: 100%; +} + +.SmartSearch-tag--deletable > .SmartSearch-name { + max-width: ~"calc(100% - 23px)"; +} + +.SmartSearch-deleteContainer:hover, +{ + border-color: @default-err; + background-color: @default-err!important; +} + +.SmartSearch-deleteContainer:hover > .SmartSearch-tagDelete { + color: @default-bg; +} +.SmartSearch-clearAll{ + font-size: 12px; + padding-top: 5px; +} +.SmartSearch-keyToggle { + margin-left: auto; + text-transform: uppercase; + background-color: @default-bg; + border-radius: 5px; + color: @default-interface-txt; + border: 1px solid @d7grey; + cursor: pointer; + width: 70px; + height: 34px; + line-height: 20px; +} + +.SmartSearch-keyToggle:hover { + background-color: @default-tertiary-bg; +} + +.SmartSearch-keyToggle.is-active { + background-color: @default-link; + border-color: @default-link; + color: @default-bg; + &:hover{ + background-color: @default-link-hov; + } +} + +.SmartSearch-keyPane { + max-height: 200px; + overflow: auto; + display: flex; + flex-wrap: wrap; + margin: 0px 0px 20px 0px; + font-size: 12px; + width: 100%; + padding: 15px; + margin-bottom: 15px; + border-radius: 4px; + border: 1px solid @login-notice-border; + background-color: @login-notice-bg; + color: @login-notice-text; +} + +.SmartSearch-relations{ + margin-top: 15px; +} + +.SmartSearch-keyRow { + width: 33%; + flex: 1 1 auto; + flex-direction: column; + margin-bottom: 15px; + padding-right: 50px; +} +// 100% rows in a modal +.modal-body .SmartSearch-keyRow{ + width: 100%; +} +// `.${list.name}List` class can be used to set add custom class overrides +.groupsList .SmartSearch-keyRow, .hostsList .SmartSearch-keyRow, .PortalMode .SmartSearch-keyRow{ + width: 100%; +} +.SmartSearch-keyRow:nth-child(3){ + padding-right: 0px; +} + +.SmartSearch-keyName { + flex: 1 0 auto; + text-transform: uppercase; + font-weight: bold; + padding-bottom: 3px; +} + +.SmartSearch-keyComparators { + flex: 1 0 auto; +} diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js new file mode 100644 index 0000000000..7de3f82886 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -0,0 +1,113 @@ +export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', + function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) { + + let path, relations, + // steps through the current tree of $state configurations, grabs default search params + defaults = _.find($state.$current.path, (step) => { + return step.params.hasOwnProperty(`${$scope.iterator}_search`); + }).params[`${$scope.iterator}_search`].config.value, + queryset = $stateParams[`${$scope.iterator}_search`]; + + // build $scope.tags from $stateParams.QuerySet, build fieldset key + init(); + + function init() { + path = GetBasePath($scope.basePath) || $scope.basePath; + relations = getRelationshipFields($scope.dataset.results); + $scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]); + qs.initFieldset(path, $scope.djangoModel, relations).then((models) => { + $scope.models = models; + }); + } + + // Removes state definition defaults and pagination terms + function stripDefaultParams(params) { + return _.pick(params, (value, key) => { + // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value + return defaults[key] !== value && key !== 'page' && key !== 'page_size' && defaults[key] !== null; + }); + } + + // searchable relationships + function getRelationshipFields(dataset) { + let flat = _(dataset).map((value) => { + return _.keys(value.related); + }).flatten().uniq().value(); + return flat; + } + + $scope.toggleKeyPane = function() { + $scope.showKeyPane = !$scope.showKeyPane; + }; + + $scope.clearAll = function(){ + let cleared = defaults; + delete cleared.page; + queryset = cleared; + $state.go('.', {[$scope.iterator + '_search']: queryset}); + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); + $scope.searchTags = stripDefaultParams(queryset); + }; + + // remove tag, merge new queryset, $state.go + $scope.remove = function(key) { + delete queryset[key]; + $state.go('.', { + [$scope.iterator + '_search']: queryset }); + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); + $scope.searchTags = stripDefaultParams(queryset); + }; + + // add a search tag, merge new queryset, $state.go() + $scope.add = function(terms) { + let params = {}; + + _.forEach(terms.split(' '), (term) => { + // if only a value is provided, search using default keys + if (term.split(':').length === 1) { + params = _.merge(params, setDefaults(term)); + } else { + params = _.merge(params, qs.encodeParam(term)); + } + }); + + function setDefaults(term) { + // "name" and "description" are sane defaults for MOST models, but not ALL! + // defaults may be configured in ListDefinition.defaultSearchParams + if ($scope.list.defaultSearchParams) { + return $scope.list.defaultSearchParams(term); + } else { + return { + or__name__icontains: term, + or__description__icontains: term + }; + } + } + + params.page = '1'; + queryset = _.merge(queryset, params); + // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic + // This transition will not reload controllers/resolves/views + // but will register new $stateParams[$scope.iterator + '_search'] terms + $state.go('.', { + [$scope.iterator + '_search']: queryset }); + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); + + $scope.searchTerm = null; + $scope.searchTags = stripDefaultParams(queryset); + }; + + $scope.decodeParam = function(key, value) { + return qs.decodeParam(key, value); + }; + } +]; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.directive.js b/awx/ui/client/src/shared/smart-search/smart-search.directive.js new file mode 100644 index 0000000000..4cf67ed51f --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/smart-search.directive.js @@ -0,0 +1,23 @@ +export default ['templateUrl', + function(templateUrl) { + return { + restrict: 'E', + replace: false, + transclude: { + actions: '?div' // preferably would transclude an actions directive here + }, + scope: { + djangoModel: '@', + searchSize: '@', + basePath: '@', + iterator: '@', + list: '=', + dataset: '=', + collection: '=', + searchTags: '=', + }, + controller: 'SmartSearchController', + templateUrl: templateUrl('shared/smart-search/smart-search') + }; + } +]; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html new file mode 100644 index 0000000000..7da08310eb --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -0,0 +1,57 @@ +
    + +
    +
    + +
    + + +
    + +
    +
    +
    + Key +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    + {{decodeParam(key, value)}} +
    +
    + CLEAR ALL +
    +
    +
    + +
    +
    + +
    +
    +
    + {{ key }} +
    +
    +
    Type: {{ value.type }}
    +
    Description: {{value.help_text}}
    +
    + Enumerated: {{ choice[0] }} +
    +
    +
    +
    + Searchable relationships: {{ relation }}, +
    +
    +
    diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index 5cd9ebfe40..af76fc736b 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -30,8 +30,7 @@ export default self.socket = new ReconnectingWebSocket(url, null, { timeoutInterval: 3000, - maxReconnectAttempts: 10 - }); + maxReconnectAttempts: 10 }); self.socket.onopen = function () { $log.debug("Websocket connection opened."); @@ -127,7 +126,7 @@ export default // listen for specific messages. A subscription object could // look like {"groups":{"jobs": ["status_changed", "summary"]}. // This is used by all socket-enabled $states - this.emit(JSON.stringify(state.socket)); + this.emit(JSON.stringify(state.data.socket)); this.setLast(state); }, unsubscribe: function(state){ @@ -136,7 +135,7 @@ export default // to the API: {"groups": {}}. // This is used for all pages that are socket-disabled if(this.requiresNewSubscribe(state)){ - this.emit(JSON.stringify(state.socket)); + this.emit(JSON.stringify(state.data.socket)); } this.setLast(state); }, @@ -151,7 +150,7 @@ export default // required an "unsubscribe", then we don't need to unsubscribe // again, b/c the UI is already unsubscribed from all groups if (this.getLast() !== undefined){ - if( _.isEmpty(state.socket.groups) && _.isEmpty(this.getLast().socket.groups)){ + if( _.isEmpty(state.data.socket.groups) && _.isEmpty(this.getLast().data.socket.groups)){ return false; } else { @@ -206,16 +205,16 @@ export default // requires a subscribe or an unsubscribe var self = this; socketPromise.promise.then(function(){ - if(!state.socket){ - state.socket = {groups: {}}; + if(!state.data && !state.data.socket){ + state.data.socket = {groups: {}}; self.unsubscribe(state); } else{ - if(state.socket.groups.hasOwnProperty( "job_events")){ - state.socket.groups.job_events = [id]; + if(state.data && state.data.socket && state.data.socket.groups.hasOwnProperty( "job_events")){ + state.data.socket.groups.job_events = [id]; } - if(state.socket.groups.hasOwnProperty( "ad_hoc_command_events")){ - state.socket.groups.ad_hoc_command_events = [id]; + if(state.data && state.data.socket && state.data.socket.groups.hasOwnProperty( "ad_hoc_command_events")){ + state.data.socket.groups.ad_hoc_command_events = [id]; } self.subscribe(state); } diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js new file mode 100644 index 0000000000..b65ecfebf5 --- /dev/null +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -0,0 +1,434 @@ +/** + * @ngdoc interface + * @name stateDefinitions + * @description An API for generating a standard set of state definitions + * generateTree - builds a full list/form tree + * generateListNode - builds a single list node e.g. {name: 'projects', ...} + * generateFormNode - builds a form node definition e.g. {name: 'projects.add', ...} + * generateFormListDefinitions - builds form list definitions attached to a form node e.g. {name: 'projects.edit.permissions', ...} + * generateLookupNodes - Attaches to a form node. Builds an abstract '*.lookup' node with field-specific 'lookup.*' children e.g. {name: 'projects.add.lookup.organizations', ...} + */ + +import { templateUrl } from './template-url/template-url.factory'; + +export default ['$injector', '$stateExtender', '$log', function($injector, $stateExtender, $log) { + return { + /** + * @ngdoc method + * @name stateDefinitions.generateTree + * @description intended for consumption by $stateProvider.state.lazyLoad in a placeholder node + * @param {object} params + { + parent: 'stateName', // the name of the top-most node of this tree + modes: ['add', 'edit'], // form modes to include in this state tree + list: 'InjectableListDefinition', + form: 'InjectableFormDefinition', + controllers: { + list: 'Injectable' || Object, + add: 'Injectable' || Object, + edit: 'Injectable' || Object, + } + * @returns {object} Promise which resolves to an object.state containing array of all state definitions in this tree + * e.g. {state: [{...}, {...}, ...]} + */ + generateTree: function(params) { + let form, list, formStates, listState, + states = []; + //return defer.promise; + return new Promise((resolve) => { + // returns array of the following states: + // resource.add, resource.edit + // resource.add.lookup, resource.add.lookup.* => [field in form.fields if field.type == 'lookup'] + // resource.edit.lookup, resource.edit.lookup.* => [field in form.fields if field.type == 'lookup'] + // resource.edit.* => [relationship in form.related] + if (params.list) { + list = $injector.get(params.list); + + listState = this.generateListNode(list, params); + states.push(listState); + } + if (params.form) { + // handle inconsistent typing of form definitions + // can be either an object or fn + form = $injector.get(params.form); + form = typeof(form) === 'function' ? form() : form; + + formStates = _.map(params.modes, (mode) => this.generateFormNode(mode, form, params)); + states = states.concat(_.flatten(formStates)); + $log.debug('*** Generated State Tree', states); + resolve({ states: states }); + } + }); + }, + + /** + * @ngdoc method + * @name stateDefinitions.generateListNode + * @description builds single list node + * @params {object} list - list definition/configuration object + * @params {object} params + * @returns {object} a list state definition + */ + generateListNode: function(list, params) { + let state; + + // allows passed-in params to specify a custom templateUrl + // otherwise, use html returned by generateList.build() to fulfill templateProvider fn + function generateTemplateBlock() { + if (params.templates && params.templates.list) { + return params.templates.list; + } else { + return function(ListDefinition, generateList) { + let html = generateList.build({ + list: ListDefinition, + mode: 'edit' + }); + html = generateList.wrapPanel(html); + return html; + }; + } + } + state = $stateExtender.buildDefinition({ + searchPrefix: list.iterator, + name: params.parent, + url: (params.url || `/${list.name}`), + data: params.data, + ncyBreadcrumb: { + label: list.title + }, + resolve: { + Dataset: [params.list, 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + ListDefinition: () => list + }, + views: { + 'list@': { + // resolves to a variable property name: + // 'templateUrl' OR 'templateProvider' + [params.templates && params.templates.list ? 'templateUrl' : 'templateProvider']: generateTemplateBlock(), + controller: params.controllers.list, + } + } + }); + // allow passed-in params to override default resolve block + if (params.resolve && params.resolve.list) { + state.resolve = _.merge(state.resolve, params.resolve.list); + } + // allow passed-in params to override default ncyBreadcrumb property + if (params.ncyBreadcrumb) { + state.ncyBreadcrumb = params.ncyBreadcrumb; + } + if (list.search) { + state.params[`${list.iterator}_search`].value = _.merge(state.params[`${list.iterator}_search`].value, list.search); + } + return state; + }, + /** + * @ngdoc method + * @name stateDefinitions.generateFormNode + * @description builds a node of form states, e.g. resource.edit.** or resource.add.** + * @param {string} mode - 'add' || 'edit' - the form mode of this node + * @param {object} form - form definition/configuration object + * @returns {array} Array of state definitions required by form mode [{...}, {...}, ...] + */ + generateFormNode: function(mode, form, params) { + let formNode, states = []; + switch (mode) { + case 'add': + formNode = $stateExtender.buildDefinition({ + name: params.name || `${params.parent}.add`, + url: params.url || '/add', + ncyBreadcrumb: { + [params.parent ? 'parent' : null]: `${params.parent}`, + label: `CREATE ${form.name}` + }, + views: { + 'form@': { + templateProvider: function(FormDefinition, GenerateForm) { + let form = typeof(FormDefinition) === 'function' ? + FormDefinition() : FormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: params.controllers.add + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }); + if (params.resolve && params.resolve.add) { + formNode.resolve = _.merge(formNode.resolve, params.resolve.add); + } + break; + case 'edit': + formNode = $stateExtender.buildDefinition({ + name: params.name || `${params.parent}.edit`, + url: (params.url || `/:${form.name}_id`), + ncyBreadcrumb: { + [params.parent ? 'parent' : null]: `${params.parent}`, + label: '{{parentObject.name || name}}' + }, + views: { + 'form@': { + templateProvider: function(FormDefinition, GenerateForm) { + let form = typeof(FormDefinition) === 'function' ? + FormDefinition() : FormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'edit' + }); + }, + controller: params.controllers.edit + } + }, + resolve: { + FormDefinition: [params.form, function(definition) { + return definition; + }], + resourceData: ['FormDefinition', 'Rest', '$stateParams', 'GetBasePath', + function(FormDefinition, Rest, $stateParams, GetBasePath) { + let form, path; + form = typeof(FormDefinition) === 'function' ? + FormDefinition() : FormDefinition; + if (GetBasePath(form.basePath) === undefined && GetBasePath(form.stateTree) === undefined ){ + throw { name: 'NotImplementedError', message: `${form.name} form definition is missing basePath or stateTree property.` }; + } + else{ + path = (GetBasePath(form.basePath) || GetBasePath(form.stateTree) || form.basePath) + $stateParams[`${form.name}_id`]; + } + Rest.setUrl(path); + return Rest.get(); + } + ] + } + }); + if (params.resolve && params.resolve.edit) { + formNode.resolve = _.merge(formNode.resolve, params.resolve.edit); + } + break; + } + states.push(formNode); + states = states.concat(this.generateLookupNodes(form, formNode)).concat(this.generateFormListDefinitions(form, formNode)); + return states; + }, + /** + * @ngdoc method + * @name stateDefinitions.generateFormListDefinitions + * @description builds state definitions for a form's related lists, like notifications/permissions + * @param {object} form - form definition/configuration object + * @params {object} formStateDefinition - the parent form node + * @returns {array} Array of state definitions [{...}, {...}, ...] + */ + generateFormListDefinitions: function(form, formStateDefinition) { + + function buildPermissionDirective() { + let states = []; + + states.push($stateExtender.buildDefinition({ + name: `${formStateDefinition.name}.permissions.add`, + squashSearchUrl: true, + url: '/add-permissions', + params: { + user_search: { + value: { order_by: 'username', page_size: '5' }, + dynamic: true, + }, + team_search: { + value: { order_by: 'name', page_size: '5' }, + dynamic: true + } + }, + views: { + [`modal@${formStateDefinition.name}`]: { + template: `` + } + }, + resolve: { + usersDataset: ['addPermissionsUsersList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams.user_search); + + } + ], + teamsDataset: ['addPermissionsTeamsList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams.team_search); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#add-permissions-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + }, + })); + return states; + } + + function buildListNodes(field) { + let states = []; + states.push(buildListDefinition(field)); + if (field.iterator === 'permission' && field.actions && field.actions.add) { + states.push(buildPermissionDirective()); + states = _.flatten(states); + } + return states; + } + + function buildListDefinition(field) { + let state, + list = field.include ? $injector.get(field.include) : field; + state = $stateExtender.buildDefinition({ + searchPrefix: `${list.iterator}`, + name: `${formStateDefinition.name}.${list.iterator}s`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${field.iterator}s` + }, + params: { + [list.iterator + '_search']: { + value: { order_by: field.order_by ? field.order_by : 'name' } + }, + }, + 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: ['$scope', 'ListDefinition', 'Dataset', + function($scope, list, Dataset) { + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[`${list.iterator}s`] = $scope[`${list.iterator}_dataset`].results; + } + ] + } + }, + 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 }); + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } + }); + // 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 _(form.related).map(buildListNodes).flatten().value(); + }, + /** + * @ngdoc method + * @name stateDefinitions.generateLookupNode + * @description builds a node of child states for each lookup field in a form + * @param {object} form - form definition/configuration object + * @params {object} formStateDefinition - the parent form node + * @returns {array} Array of state definitions [{...}, {...}, ...] + */ + generateLookupNodes: function(form, formStateDefinition) { + + function buildFieldDefinition(field) { + let state = $stateExtender.buildDefinition({ + searchPrefix: field.sourceModel, + squashSearchUrl: true, + name: `${formStateDefinition.name}.${field.sourceModel}`, + url: `/${field.sourceModel}`, + // a lookup field's basePath takes precedence over generic list definition's basePath, if supplied + data: { + basePath: field.basePath || null, + lookup: true + }, + params: { + [field.sourceModel + '_search']: { + value: { page_size: '5' } + } + }, + views: { + 'modal': { + templateProvider: function(ListDefinition, generateList) { + let list_html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + return `${list_html}`; + + } + } + }, + resolve: { + ListDefinition: [field.list, function(list) { + return list; + }], + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', '$state', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope, $state) => { + // allow lookup field definitions to use interpolated $stateParams / $rootScope in basePath field + // the basePath on a form's lookup field will take precedence over the general model list's basepath + let path, interpolator; + if ($state.transition._targetState._definition.data && GetBasePath($state.transition._targetState._definition.data.basePath)) { + path = GetBasePath($state.transition._targetState._definition.data.basePath); + } else if ($state.transition._targetState._definition.data && $state.transition._targetState._definition.data.basePath) { + interpolator = $interpolate($state.transition._targetState._definition.data.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } else if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#form-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + }, + }); + if (field.search) { + state.params[`${field.sourceModel}_search`].value = _.merge(state.params[`${field.sourceModel}_search`].value, field.search); + } + return state; + } + return _(form.fields).filter({ type: 'lookup' }).map(buildFieldDefinition).value(); + } + + }; + +}]; diff --git a/awx/ui/client/src/shared/stateExtender.provider.js b/awx/ui/client/src/shared/stateExtender.provider.js index 9c5bf0bfae..da8b34fbee 100644 --- a/awx/ui/client/src/shared/stateExtender.provider.js +++ b/awx/ui/client/src/shared/stateExtender.provider.js @@ -1,10 +1,11 @@ export default function($stateProvider) { this.$get = function() { return { + // attaches socket as resolvable if specified in state definition addSocket: function(state){ // The login route has a 'null' socket because it should // neither subscribe or unsubscribe - if(state.socket!==null){ + if(state.data && state.data.socket!==null){ if(!state.resolve){ state.resolve = {}; } @@ -15,24 +16,58 @@ export default function($stateProvider) { ]; } }, + // builds a state definition with default slaw + buildDefinition: function(state) { + let params, defaults, definition, + searchPrefix = state.searchPrefix ? `${state.searchPrefix}_search` : null, + route = state.route || state.url; - addState: function(state) { - var route = state.route || state.url; - this.addSocket(state); - $stateProvider.state(state.name, { + if (searchPrefix) { + defaults = { + params: { + [searchPrefix]: { + value: { + page_size: "20", + order_by: "name" + }, + dynamic: true, + squash: true + } + } + }; + route = !state.squashSearchUrl ? `${route}?{${searchPrefix}:queryset}` : route; + params = state.params === undefined ? defaults.params : _.merge(defaults.params, state.params); + } else { + params = state.params; + } + + definition = { + name: state.name, url: route, + abstract: state.abstract, controller: state.controller, templateUrl: state.templateUrl, + templateProvider: state.templateProvider, resolve: state.resolve, - params: state.params, + params: params, data: state.data, ncyBreadcrumb: state.ncyBreadcrumb, onEnter: state.onEnter, onExit: state.onExit, template: state.template, controllerAs: state.controllerAs, - views: state.views - }); + views: state.views, + parent: state.parent, + // new in uiRouter 1.0 + lazyLoad: state.lazyLoad, + }; + this.addSocket(definition); + return definition; + }, + // registers a state definition with $stateProvider service + addState: function(state) { + let definition = this.buildDefinition(state); + $stateProvider.state(state.name, definition); } }; }; diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js index 1b7d546231..e9510deb62 100644 --- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js @@ -4,24 +4,24 @@ * All Rights Reserved *************************************************/ -import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { templateUrl } from '../../shared/template-url/template-url.factory'; export default { name: 'adHocJobStdout', route: '/ad_hoc_commands/:id', templateUrl: templateUrl('standard-out/adhoc/standard-out-adhoc'), controller: 'JobStdoutController', - socket: { - "groups":{ - "jobs": ["status_changed"], - "ad_hoc_command_events": [] - } - }, ncyBreadcrumb: { parent: "jobs", label: "{{ job.module_name }}" }, data: { - jobType: 'ad_hoc_commands' + jobType: 'ad_hoc_commands', + socket: { + "groups": { + "jobs": ["status_changed"], + "ad_hoc_command_events": [] + } + } } }; diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js index c93cbd7a92..75472922b8 100644 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js @@ -4,23 +4,23 @@ * All Rights Reserved *************************************************/ -import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { templateUrl } from '../../shared/template-url/template-url.factory'; export default { name: 'managementJobStdout', route: '/management_jobs/:id', templateUrl: templateUrl('standard-out/management-jobs/standard-out-management-jobs'), controller: 'JobStdoutController', - socket: { - "groups":{ - "jobs": ["status_changed"] - } - }, ncyBreadcrumb: { parent: "jobs", label: "{{ job.name }}" }, data: { - jobType: 'system_jobs' + jobType: 'system_jobs', + socket: { + "groups": { + "jobs": ["status_changed"] + } + } } }; diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js index d027050444..87d0942802 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { templateUrl } from '../../shared/template-url/template-url.factory'; // TODO: figure out what this route should be - should it be scm_update? @@ -17,12 +17,12 @@ export default { parent: "jobs", label: "{{ project_name }}" }, - socket: { - "groups":{ - "jobs": ["status_changed"] - } - }, data: { - jobType: 'project_updates' + jobType: 'project_updates', + socket: { + "groups": { + "jobs": ["status_changed"] + } + }, } }; diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index bd9b0011a4..ccd5ad0173 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -18,8 +18,7 @@ import listGenerator from '../shared/list-generator/main'; -angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginationHelpers', - 'RefreshHelper', listGenerator.name, 'StreamWidget', +angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', listGenerator.name, 'StreamWidget', ]) .factory('BuildAnchor', [ '$log', '$filter', @@ -222,12 +221,11 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti .factory('ShowDetail', ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'ActivityDetailForm', 'Empty', 'Find', function ($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) { - return function (params) { + return function (params, scope) { var activity_id = params.activity_id, - parent_scope = params.scope, - activity = Find({ list: parent_scope.activities, key: 'id', val: activity_id }), - scope, element; + activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }), + element; if (activity) { @@ -259,19 +257,16 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti ]) .factory('Stream', ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath', - 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', 'PaginateInit', - 'generateList', 'FormatDate', 'BuildDescription', + 'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription', 'ShowDetail', function ($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors, - Wait, StreamList, SearchInit, PaginateInit, GenerateList, FormatDate, + Wait, StreamList, GenerateList, FormatDate, BuildDescription, ShowDetail) { return function (params) { var list = _.cloneDeep(StreamList), defaultUrl = GetBasePath('activity_stream'), - view = GenerateList, - parent_scope = params.scope, - scope = parent_scope.$new(), + scope = params.scope, url = (params && params.url) ? params.url : null; $rootScope.flashMessage = null; @@ -391,13 +386,13 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti list.basePath = defaultUrl; // Generate the list - view.inject(list, { mode: 'edit', id: 'stream-content', searchSize: 'col-lg-4 col-md-4 col-sm-12 col-xs-12', secondWidget: true, activityStream: true, scope: scope }); + //view.inject(list, { mode: 'edit', id: 'stream-content', searchSize: 'col-lg-4 col-md-4 col-sm-12 col-xs-12', secondWidget: true, activityStream: true, scope: scope }); // descriptive title describing what AS is showing scope.streamTitle = (params && params.title) ? params.title : null; scope.refreshStream = function () { - scope.search(list.iterator); + $state.go('.', null, {reload: true}); }; scope.showDetail = function (id) { @@ -407,37 +402,18 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti }); }; - if (scope.removeStreamPostRefresh) { - scope.removeStreamPostRefresh(); - } - scope.removeStreamPostRefresh = scope.$on('PostRefresh', function () { - scope.activities.forEach(function(activity, i) { - // build activity.user - if (scope.activities[i].summary_fields.actor) { - scope.activities[i].user = "" + - scope.activities[i].summary_fields.actor.username + ""; - } else { - scope.activities[i].user = 'system'; - } - // build description column / action text - BuildDescription(scope.activities[i]); + scope.activities.forEach(function(activity, i) { + // build activity.user + if (scope.activities[i].summary_fields.actor) { + scope.activities[i].user = "" + + scope.activities[i].summary_fields.actor.username + ""; + } else { + scope.activities[i].user = 'system'; + } + // build description column / action text + BuildDescription(scope.activities[i]); - }); }); - - // Initialize search and paginate pieces and load data - SearchInit({ - scope: scope, - set: list.name, - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: scope, - list: list, - url: defaultUrl - }); - scope.search(list.iterator); }; } ]); diff --git a/awx/ui/karma.conf.js b/awx/ui/karma.conf.js index dbeab4a306..1a22f476b2 100644 --- a/awx/ui/karma.conf.js +++ b/awx/ui/karma.conf.js @@ -20,10 +20,12 @@ module.exports = function(config) { './client/src/app.js', './node_modules/angular-mocks/angular-mocks.js', { pattern: './tests/**/*-test.js' }, + 'client/src/**/*.html' ], preprocessors: { './client/src/app.js': ['webpack', 'sourcemap'], './tests/**/*-test.js': ['webpack', 'sourcemap'], + 'client/src/**/*.html': ['html2js'] }, webpack: { plugins: [ diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index e22bbbc3f6..656c931f8b 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -182,9 +182,9 @@ } }, "angular-ui-router": { - "version": "0.2.18", - "from": "angular-ui-router@>=0.2.15 <0.3.0", - "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-0.2.18.tgz" + "version": "1.0.0-beta.3", + "from": "angular-ui-router@>=1.0.0-beta.3 <2.0.0", + "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-1.0.0-beta.3.tgz" }, "ansi-regex": { "version": "2.0.0", @@ -892,9 +892,9 @@ } }, "caniuse-db": { - "version": "1.0.30000568", + "version": "1.0.30000569", "from": "caniuse-db@>=1.0.30000554 <2.0.0", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000568.tgz" + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000569.tgz" }, "caseless": { "version": "0.11.0", @@ -1645,7 +1645,7 @@ }, "for-own": { "version": "0.1.4", - "from": "for-own@>=0.1.3 <0.2.0", + "from": "for-own@>=0.1.4 <0.2.0", "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz" }, "forever-agent": { @@ -3347,9 +3347,9 @@ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz" }, "object.omit": { - "version": "2.0.0", + "version": "2.0.1", "from": "object.omit@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz" }, "on-finished": { "version": "2.3.0", diff --git a/awx/ui/package.json b/awx/ui/package.json index 0ad7504597..3383594e82 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -86,7 +86,7 @@ "angular-sanitize": "^1.4.3", "angular-scheduler": "chouseknecht/angular-scheduler#0.1.0", "angular-tz-extensions": "chouseknecht/angular-tz-extensions#0.3.11", - "angular-ui-router": "^0.2.15", + "angular-ui-router": "^1.0.0-beta.3", "bootstrap": "^3.1.1", "bootstrap-datepicker": "^1.4.0", "codemirror": "^5.17.0", diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index d5777c9619..918dead3c6 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -1,180 +1,148 @@ + - - - Ansible Tower - - - - - - - - - - - - - - - - - - - - + + + - + + - - -
    - - +
    + + +
    +
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - + +
    +
    +
    +

    working...

    +
    - -
    -

    working...

    - - + + diff --git a/awx/ui/tests/spec/lookup/lookup-modal.directive-test.js b/awx/ui/tests/spec/lookup/lookup-modal.directive-test.js new file mode 100644 index 0000000000..cdd4d16cc9 --- /dev/null +++ b/awx/ui/tests/spec/lookup/lookup-modal.directive-test.js @@ -0,0 +1,131 @@ +'use strict'; + +xdescribe('Directive: lookupModal', () => { + + let dom, element, listHtml, listDefinition, Dataset, + lookupTemplate, paginateTemplate, searchTemplate, columnSortTemplate, + $scope, $parent, $compile, $state; + + // mock dependency chains + // (shared requires RestServices requires Authorization etc) + beforeEach(angular.mock.module('login')); + beforeEach(angular.mock.module('shared')); + + beforeEach(angular.mock.module('LookupModalModule', ($provide) => { + $provide.value('smartSearch', angular.noop); + $provide.value('columnSort', angular.noop); + $provide.value('paginate', angular.noop); + $state = jasmine.createSpyObj('$state', ['go']); + })); + + beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_, _generateList_) => { + listDefinition = { + name: 'mocks', + iterator: 'mock', + fields: { + name: {} + } + }; + + listHtml = _generateList_.build({ + mode: 'lookup', + list: listDefinition, + input_type: 'radio' + }); + + Dataset = { + data: { + results: [ + { id: 1, name: 'Mock Resource 1' }, + { id: 2, name: 'Mock Resource 2' }, + { id: 3, name: 'Mock Resource 3' }, + { id: 4, name: 'Mock Resource 4' }, + { id: 5, name: 'Mock Resource 5' }, + ] + } + }; + + dom = angular.element(`${listHtml}`); + + // populate $templateCache with directive.templateUrl at test runtime, + lookupTemplate = window.__html__['client/src/shared/lookup/lookup-modal.partial.html']; + paginateTemplate = window.__html__['client/src/shared/paginate/paginate.partial.html']; + searchTemplate = window.__html__['client/src/shared/smart-search/smart-search.partial.html']; + columnSortTemplate = window.__html__['client/src/shared/column-sort/column-sort.partial.html']; + + $templateCache.put('/static/partials/shared/lookup/lookup-modal.partial.html', lookupTemplate); + $templateCache.put('/static/partials/shared/paginate/paginate.partial.html', paginateTemplate); + $templateCache.put('/static/partials/shared/smart-search/smart-search.partial.html', searchTemplate); + $templateCache.put('/static/partials/shared/column-sort/column-sort.partial.html', columnSortTemplate); + + $compile = _$compile_; + $parent = _$rootScope_.$new(); + + // mock resolvables + $scope = $parent.$new(); + $scope.$resolve = { + ListDefinition: listDefinition, + Dataset: Dataset + }; + })); + + it('Resource is pre-selected in form - corresponding radio should initialize checked', () => { + $parent.mock = 1; // resource id + $parent.mock_name = 'Mock Resource 1'; // resource name + + console.log($scope); + + element = $compile(dom)($scope); + $scope.$digest(); + + expect($(':radio')[0].is(':checked')).toEqual(true); + }); + + it('No resource pre-selected in form - no radio should initialize checked', () => { + element = $compile(dom)($scope); + $scope.$digest(); + + _.forEach($(':radio'), (radio) => { + expect(radio.is('checked')).toEqual(false); + }); + }); + + it('Should update $parent / form scope and exit $state on save', () => { + element = $compile(dom)($scope); + $scope.$digest(); + $(':radio')[1].click(); + $('.Lookup-save')[0].click(); + + expect($parent.mock).toEqual(2); + expect($parent.mock_name).toEqual('Mock Resource 2'); + expect($state.go).toHaveBeenCalled(); + }); + + it('Should not update $parent / form scope on exit via header', () => { + $parent.mock = 3; // resource id + $parent.mock_name = 'Mock Resource 3'; // resource name + element = $compile(dom)($scope); + $scope.$digest(); + + $(':radio')[1].click(); + $('.Form-exit')[0].click(); + + expect($parent.mock).toEqual(3); + expect($parent.mock_name).toEqual('Mock Resource 3'); + expect($state.go).toHaveBeenCalled(); + }); + + it('Should not update $parent / form scope on exit via cancel button', () => { + $parent.mock = 3; // resource id + $parent.mock_name = 'Mock Resource 3'; // resource name + element = $compile(dom)($scope); + $scope.$digest(); + + $(':radio')[1].click(); + $('.Lookup-cancel')[0].click(); + + expect($parent.mock).toEqual(3); + expect($parent.mock_name).toEqual('Mock Resource 3'); + expect($state.go).toHaveBeenCalled(); + }); +}); diff --git a/awx/ui/tests/spec/paginate/paginate.directive-test.js b/awx/ui/tests/spec/paginate/paginate.directive-test.js new file mode 100644 index 0000000000..b8d1802725 --- /dev/null +++ b/awx/ui/tests/spec/paginate/paginate.directive-test.js @@ -0,0 +1,116 @@ +'use strict'; + +xdescribe('Directive: Paginate', () => { + var dom = angular.element(''), + template, + element, + $scope, + $compile, + $state, + $stateParams = {}; + + beforeEach(angular.mock.module('shared'), ($provide) =>{ + $provide.value('Rest', angular.noop); + }); + beforeEach(angular.mock.module('PaginateModule', ($provide) => { + $state = jasmine.createSpyObj('$state', ['go']); + + $provide.value('$stateParams', $stateParams); + $provide.value('Rest', angular.noop); + })); + beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_) => { + // populate $templateCache with directive.templateUrl at test runtime, + template = window.__html__['client/src/shared/paginate/paginate.partial.html']; + $templateCache.put('/static/partials/shared/paginate/paginate.partial.html', template); + + $compile = _$compile_; + $scope = _$rootScope_.$new(); + })); + + it('should be hidden if only 1 page of data', () => { + + $scope.mock_dataset = {count: 19}; + $scope.pageSize = 20; + element = $compile(dom)($scope); + $scope.$digest(); + + expect($('.Paginate-wrapper', element)).hasClass('ng-hide'); + }); + describe('it should show expected page range', () => { + + + it('should show 7 pages', () =>{ + + $scope.pageSize = 1; + $scope.mock_dataset = {count: 7}; + element = $compile(dom)($scope); + $scope.$digest(); + // next, previous, 7 pages + expect($('.Paginate-controls--item', element)).length.toEqual(9); + }); + it('should show 100 pages', () =>{ + $scope.pageSize = 1; + $scope.mock_dataset = {count: 100}; + element = $compile(dom)($scope); + $scope.$digest(); + // first, next, previous, last, 100 pages + expect($('.Paginate-controls--item', element)).length.toEqual(104); + }); + }); + describe('it should get expected page', () => { + + it('should get the next page', () =>{ + + $scope.mock_dataset = { + count: 42, + }; + + $stateParams.mock_search = { + page_size: 5, + page: 1 + }; + + element = $compile(dom)($scope); + $('.Paginate-controls--next').click(); + expect($stateParams.mock_search.page).toEqual(2); + }); + + it('should get the previous page', ()=>{ + + $scope.mock_dataset = { + count: 42 + }; + $stateParams.mock_search = { + page_size: 10, + page: 3 + }; + + element = $compile(dom)($scope); + $('.Paginate-controls--previous'); + expect($stateParams.mock_search.page).toEqual(2); + }); + it('should get the last page', ()=>{ + $scope.mock_dataset = { + count: 110 + }; + $stateParams.mock_search = { + page_size: 5, + page: 1 + }; + $('.Paginate-controls--last').click(); + expect($stateParams.mock_search.page).toEqual(42); + }); + it('should get the first page', () => { + $scope.mock_dataset = { + count: 110 + }; + $stateParams.mock_search = { + page_size: 5, + page: 35 + }; + $('.Paginate-controls--first').click(); + expect($stateParams.mock_search.page).toEqual(1); + }); + + }); +}); diff --git a/awx/ui/tests/spec/smart-search/queryset.service-test.js b/awx/ui/tests/spec/smart-search/queryset.service-test.js new file mode 100644 index 0000000000..54f9794e41 --- /dev/null +++ b/awx/ui/tests/spec/smart-search/queryset.service-test.js @@ -0,0 +1,85 @@ +'use strict'; + +describe('Service: QuerySet', () => { + let $httpBackend, + QuerySet, + Authorization; + + beforeEach(angular.mock.module('Tower', ($provide) =>{ + // @todo: improve app source / write testing utilities for interim + // we don't want to be concerned with this provision in every test that involves the Rest module + Authorization = { + getToken: () => true, + isUserLoggedIn: angular.noop + }; + $provide.value('LoadBasePaths', angular.noop); + $provide.value('Authorization', Authorization); + })); + beforeEach(angular.mock.module('RestServices')); + + beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => { + $httpBackend = _$httpBackend_; + QuerySet = _QuerySet_; + + // @todo: improve app source + // config.js / local_settings emit $http requests in the app's run block + $httpBackend + .whenGET(/\/static\/*/) + .respond(200, {}); + // @todo: improve appsource + // provide api version via package.json config block + $httpBackend + .whenGET('/api/') + .respond(200, ''); + })); + + describe('fn encodeQuery', () => { + xit('null/undefined params should return an empty string', () => { + expect(QuerySet.encodeQuery(null)).toEqual(''); + expect(QuerySet.encodeQuery(undefined)).toEqual(''); + }); + xit('should encode params to a string', () => { + let params = { + or__created_by: 'Jenkins', + or__modified_by: 'Jenkins', + and__not__status: 'success', + }, + result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success'; + expect(QuerySet.encodeQuery(params)).toEqual(result); + }); + }); + + xdescribe('fn decodeQuery', () => { + + }); + + + describe('fn search', () => { + let pattern = /\/api\/v1\/inventories\/(.+)\/groups\/*/, + endpoint = '/api/v1/inventories/1/groups/', + params = { + or__name: 'borg', + or__description__icontains: 'assimilate' + }; + + it('should GET expected URL', () =>{ + $httpBackend + .expectGET(pattern) + .respond(200, {}); + QuerySet.search(endpoint, params).then((data) =>{ + expect(data.config.url).toEqual('/api/v1/inventories/1/groups/?or__name=borg&or__description__icontains=assimilate'); + }); + $httpBackend.flush(); + }); + + xit('should memoize new DjangoModel', ()=>{}); + xit('should not replace memoized DjangoModel', ()=>{}); + xit('should provide an alias interface', ()=>{}); + + afterEach(() => { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + }); + +}); diff --git a/awx/ui/tests/spec/smart-search/smart-search.directive-test.js b/awx/ui/tests/spec/smart-search/smart-search.directive-test.js new file mode 100644 index 0000000000..59a701e9bc --- /dev/null +++ b/awx/ui/tests/spec/smart-search/smart-search.directive-test.js @@ -0,0 +1,163 @@ +'use strict'; + +xdescribe('Directive: Smart Search', () => { + let $scope, + template, + element, + dom, + $compile, + $state = {}, + $stateParams, + GetBasePath, + QuerySet; + + beforeEach(angular.mock.module('shared')); + beforeEach(angular.mock.module('SmartSearchModule', ($provide) => { + QuerySet = jasmine.createSpyObj('QuerySet', ['decodeParam']); + QuerySet.decodeParam.and.callFake((key, value) => { + return `${key.split('__').join(':')}:${value}`; + }); + GetBasePath = jasmine.createSpy('GetBasePath'); + + $provide.value('QuerySet', QuerySet); + $provide.value('GetBasePath', GetBasePath); + $provide.value('$state', $state); + })); + beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_) => { + // populate $templateCache with directive.templateUrl at test runtime, + template = window.__html__['client/src/shared/smart-search/smart-search.partial.html']; + $templateCache.put('/static/partials/shared/smart-search/smart-search.partial.html', template); + + $compile = _$compile_; + $scope = _$rootScope_.$new(); + })); + + describe('initializing tags', () => { + beforeEach(() => { + QuerySet.initFieldset = function() { + return { + then: function() { + return; + } + }; + }; + }); + // some defaults like page_size and page will always be provided + // but should be squashed if initialized with default values + it('should not create tags', () => { + $state.$current = { + params: { + mock_search: { + config: { + value: { + page_size: '20', + order_by: '-finished', + page: '1' + } + } + } + } + }; + $state.params = { + mock_search: { + page_size: '20', + order_by: '-finished', + page: '1' + } + }; + dom = angular.element(` + `); + element = $compile(dom)($scope); + $scope.$digest(); + expect($('.SmartSearch-tagContainer', element).length).toEqual(0); + }); + // set one possible default (order_by) with a custom value, but not another default (page_size) + it('should create an order_by tag, but not a page_size tag', () => { + $state.$current = { + params: { + mock_search: { + config: { + value: { + page_size: '20', + order_by: '-finished' + } + } + } + } + }; + $state.params = { + mock_search: { + page_size: '20', + order_by: 'name' + } + }; + dom = angular.element(` + `); + element = $compile(dom)($scope); + $scope.$digest(); + expect($('.SmartSearch-tagContainer', element).length).toEqual(1); + expect($('.SmartSearch-tagContainer .SmartSearch-name', element)[0].innerText).toEqual('order_by:name'); + }); + // set many possible defaults and many non-defaults - page_size and page shouldn't generate tags, even when non-default values are set + it('should create an order_by tag, name tag, description tag - but not a page_size or page tag', () => { + $state.$current = { + params: { + mock_search: { + config: { + value: { + page_size: '20', + order_by: '-finished', + page: '1' + } + } + } + } + }; + $state.params = { + mock_search: { + page_size: '25', + order_by: 'name', + page: '11', + description_icontains: 'ansible', + name_icontains: 'ansible' + } + }; + dom = angular.element(` + `); + element = $compile(dom)($scope); + $scope.$digest(); + expect($('.SmartSearch-tagContainer', element).length).toEqual(3); + }); + }); + + describe('removing tags', () => { + // assert a default value is still provided after a custom tag is removed + xit('should revert to state-defined order_by when order_by tag is removed', () => {}); + }); + + describe('accessing model', () => { + xit('should retrieve cached model OPTIONS from localStorage', () => {}); + xit('should call QuerySet service to retrieve unstored model OPTIONS', () => {}); + }); +});
    {{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.
    "; + innerTable += "
    "; - for (field_action in list.fieldActions) { - if (field_action !== 'columnClass') { - if (list.fieldActions[field_action].type && list.fieldActions[field_action].type === 'DropDown') { - innerTable += DropDown({ - list: list, - fld: field_action, - options: options, - base: base, - type: 'fieldActions', - td: false + for (field_action in list.fieldActions) { + if (field_action !== 'columnClass') { + if (list.fieldActions[field_action].type && list.fieldActions[field_action].type === 'DropDown') { + innerTable += DropDown({ + list: list, + fld: field_action, + options: options, + base: base, + type: 'fieldActions', + td: false + }); + } else { + fAction = list.fieldActions[field_action]; + innerTable += ""; } + //html += (fAction.label) ? " " + list.fieldActions[field_action].label + + // "" : ""; + innerTable += ""; } } - innerTable += "
    " + - loading + "
    ') - .addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus') - .append( - $('') - .attr('selections-empty', 'selectedItems.length === 0') - .attr('items-length', list.name + '.length') - .attr('label', '')); - } - - if (options === undefined) { - options = this.options; - } - - html = "
    #"; - } - html += "Select"; - html += (list.fieldActions.label === undefined || list.fieldActions.label) ? i18n._("Actions") : ""; - html += "
    ') + .addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus') + .append( + $('') + .attr('selections-empty', 'selectedItems.length === 0') + .attr('items-length', list.name + '.length') + .attr('label', '')); + } + + if (options === undefined) { + options = this.options; + } + + html = "
    # + + Select"; + html += (list.fieldActions.label === undefined || list.fieldActions.label) ? i18n._("Actions") : ""; + html += "