From 27fb54b2a776157ec9f00b78a1cfce08d09574d0 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 27 Jan 2014 18:37:51 -0500 Subject: [PATCH 01/22] AC-984 Prevent signal handlers from being run unnecessarily when deleting inventory, remove unnecessary extra queries, use update_fields when possible. --- awx/api/generics.py | 4 +- awx/api/views.py | 6 ++ awx/main/middleware.py | 30 +++--- awx/main/models/activity_stream.py | 11 ++ awx/main/models/base.py | 10 +- awx/main/models/inventory.py | 15 +-- awx/main/signals.py | 162 ++++++++++++++++++----------- 7 files changed, 153 insertions(+), 85 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 37618c5b27..3c098820ff 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -6,7 +6,7 @@ import inspect import json # Django -from django.http import HttpResponse, Http404 +from django.http import Http404 from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string @@ -428,4 +428,4 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, generics.RetrieveUpdat obj.mark_inactive() else: raise NotImplementedError('destroy() not implemented yet for %s' % obj) - return HttpResponse(status=204) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/awx/api/views.py b/awx/api/views.py index ac7305f5be..f2aa3530b7 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -34,6 +34,7 @@ from awx.main.licenses import LicenseReader from awx.main.models import * from awx.main.utils import * from awx.main.access import get_user_queryset +from awx.main.signals import ignore_inventory_computed_fields, ignore_inventory_group_removal from awx.api.authentication import JobTaskAuthentication from awx.api.permissions import * from awx.api.serializers import * @@ -619,6 +620,11 @@ class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventorySerializer + def destroy(self, request, *args, **kwargs): + with ignore_inventory_computed_fields(): + with ignore_inventory_group_removal(): + return super(InventoryDetail, self).destroy(request, *args, **kwargs) + class InventoryActivityStreamList(SubListAPIView): model = ActivityStream diff --git a/awx/main/middleware.py b/awx/main/middleware.py index ce6a88b13f..c8f91f2e75 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -8,17 +8,18 @@ from django.db import IntegrityError from django.utils.functional import curry from awx.main.models import ActivityStream, AuthToken import json +import threading import uuid import urllib2 import logging logger = logging.getLogger('awx.main.middleware') -class ActivityStreamMiddleware(object): +class ActivityStreamMiddleware(threading.local): def __init__(self): self.disp_uid = None - self.instances = [] + self.instance_ids = [] def process_request(self, request): if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): @@ -28,6 +29,7 @@ class ActivityStreamMiddleware(object): set_actor = curry(self.set_actor, user) self.disp_uid = str(uuid.uuid1()) + self.instance_ids = [] post_save.connect(set_actor, sender=ActivityStream, dispatch_uid=self.disp_uid, weak=False) def process_response(self, request, response): @@ -35,31 +37,27 @@ class ActivityStreamMiddleware(object): drf_user = getattr(drf_request, 'user', None) if self.disp_uid is not None: post_save.disconnect(dispatch_uid=self.disp_uid) - for instance_id in self.instances: - instance = ActivityStream.objects.filter(id=instance_id) - if instance.exists(): - instance = instance[0] - else: - logger.debug("Failed to look up Activity Stream instance for id : " + str(instance_id)) - continue - if drf_user is not None and drf_user.__class__ != AnonymousUser: + for instance in ActivityStream.objects.filter(id__in=self.instance_ids): + if drf_user and drf_user.pk: instance.actor = drf_user try: - instance.save() + instance.save(update_fields=['actor']) except IntegrityError, e: - logger.debug("Integrity Error saving Activity Stream instance for id : " + str(instance_id)) + logger.debug("Integrity Error saving Activity Stream instance for id : " + str(instance.id)) # else: # obj1_type_actual = instance.object1_type.split(".")[-1] # if obj1_type_actual in ("InventoryUpdate", "ProjectUpdate", "Job") and instance.id is not None: # instance.delete() + + self.instance_ids = [] return response def set_actor(self, user, sender, instance, **kwargs): if sender == ActivityStream: - if isinstance(user, User) and instance.user is None: + if isinstance(user, User) and instance.actor is None: instance.actor = user - instance.save() + instance.save(update_fields=['actor']) else: - if instance.id not in self.instances: - self.instances.append(instance.id) + if instance.id not in self.instance_ids: + self.instance_ids.append(instance.id) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index ed7a1c5fba..a41f14f0d1 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -51,3 +51,14 @@ class ActivityStream(models.Model): def get_absolute_url(self): return reverse('api:activity_stream_detail', args=(self.pk,)) + + def save(self, *args, **kwargs): + # For compatibility with Django 1.4.x, attempt to handle any calls to + # save that pass update_fields. + try: + super(ActivityStream, self).save(*args, **kwargs) + except TypeError: + if 'update_fields' not in kwargs: + raise + kwargs.pop('update_fields') + super(ActivityStream, self).save(*args, **kwargs) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index e6653881cf..0c3de0af7e 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -193,14 +193,20 @@ class PrimordialModel(BaseModel): tags = TaggableManager(blank=True) - def mark_inactive(self, save=True): + def mark_inactive(self, save=True, update_fields=None): '''Use instead of delete to rename and mark inactive.''' + update_fields = update_fields or [] if self.active: if 'name' in self._meta.get_all_field_names(): self.name = "_deleted_%s_%s" % (now().isoformat(), self.name) + if 'name' not in update_fields: + update_fields.append('name') self.active = False + if 'active' not in update_fields: + update_fields.append('active') if save: - self.save() + self.save(update_fields=update_fields) + return update_fields def clean_description(self): # Description should always be empty string, never null. diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index dfadfd8bd4..87550cadc8 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -100,14 +100,15 @@ class Inventory(CommonModel): ''' When marking inventory inactive, also mark hosts and groups inactive. ''' + from awx.main.signals import ignore_inventory_computed_fields + with ignore_inventory_computed_fields(): + for host in self.hosts.filter(active=True): + host.mark_inactive() + for group in self.groups.filter(active=True): + group.mark_inactive() + for inventory_source in self.inventory_sources.filter(active=True): + inventory_source.mark_inactive() super(Inventory, self).mark_inactive(save=save) - for host in self.hosts.filter(active=True): - host.mark_inactive() - for group in self.groups.filter(active=True): - group.mark_inactive() - group.inventory_source.mark_inactive() - for inventory_source in self.inventory_sources.filter(active=True): - inventory_source.mark_inactive() variables_dict = VarsDictProperty('variables') diff --git a/awx/main/signals.py b/awx/main/signals.py index efb00f69a2..37b1923f3b 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -24,7 +24,7 @@ logger = logging.getLogger('awx.main.signals') # or marked inactive, when a Host-Group or Group-Group relationship is updated, # or when a Job is deleted or marked inactive. -_inventory_updating = threading.local() +_inventory_updates = threading.local() @contextlib.contextmanager def ignore_inventory_computed_fields(): @@ -32,49 +32,62 @@ def ignore_inventory_computed_fields(): Context manager to ignore updating inventory computed fields. ''' try: - previous_value = getattr(_inventory_updating, 'is_updating', False) - _inventory_updating.is_updating = True + previous_value = getattr(_inventory_updates, 'is_updating', False) + _inventory_updates.is_updating = True yield finally: - _inventory_updating.is_updating = previous_value + _inventory_updates.is_updating = previous_value + +@contextlib.contextmanager +def ignore_inventory_group_removal(): + ''' + Context manager to ignore moving groups/hosts when group is deleted. + ''' + try: + previous_value = getattr(_inventory_updates, 'is_removing', False) + _inventory_updates.is_removing = True + yield + finally: + _inventory_updates.is_removing = previous_value def update_inventory_computed_fields(sender, **kwargs): ''' Signal handler and wrapper around inventory.update_computed_fields to prevent unnecessary recursive calls. ''' - if not getattr(_inventory_updating, 'is_updating', False): - instance = kwargs['instance'] - if sender == Group.hosts.through: - sender_name = 'group.hosts' - elif sender == Group.parents.through: - sender_name = 'group.parents' - elif sender == Host.inventory_sources.through: - sender_name = 'host.inventory_sources' - elif sender == Group.inventory_sources.through: - sender_name = 'group.inventory_sources' - else: - sender_name = unicode(sender._meta.verbose_name) - if kwargs['signal'] == post_save: - if sender == Job and instance.active: - return - sender_action = 'saved' - elif kwargs['signal'] == post_delete: - sender_action = 'deleted' - elif kwargs['signal'] == m2m_changed and kwargs['action'] in ('post_add', 'post_remove', 'post_clear'): - sender_action = 'changed' - else: + if getattr(_inventory_updates, 'is_updating', False): + return + instance = kwargs['instance'] + if sender == Group.hosts.through: + sender_name = 'group.hosts' + elif sender == Group.parents.through: + sender_name = 'group.parents' + elif sender == Host.inventory_sources.through: + sender_name = 'host.inventory_sources' + elif sender == Group.inventory_sources.through: + sender_name = 'group.inventory_sources' + else: + sender_name = unicode(sender._meta.verbose_name) + if kwargs['signal'] == post_save: + if sender == Job and instance.active: return - logger.debug('%s %s, updating inventory computed fields: %r %r', - sender_name, sender_action, sender, kwargs) - with ignore_inventory_computed_fields(): - try: - inventory = instance.inventory - except Inventory.DoesNotExist: - pass - else: - update_hosts = issubclass(sender, Job) - inventory.update_computed_fields(update_hosts=update_hosts) + sender_action = 'saved' + elif kwargs['signal'] == post_delete: + sender_action = 'deleted' + elif kwargs['signal'] == m2m_changed and kwargs['action'] in ('post_add', 'post_remove', 'post_clear'): + sender_action = 'changed' + else: + return + logger.debug('%s %s, updating inventory computed fields: %r %r', + sender_name, sender_action, sender, kwargs) + with ignore_inventory_computed_fields(): + try: + inventory = instance.inventory + except Inventory.DoesNotExist: + pass + else: + update_hosts = issubclass(sender, Job) + inventory.update_computed_fields(update_hosts=update_hosts) post_save.connect(update_inventory_computed_fields, sender=Host) post_delete.connect(update_inventory_computed_fields, sender=Host) @@ -94,32 +107,50 @@ post_delete.connect(update_inventory_computed_fields, sender=InventorySource) @receiver(pre_delete, sender=Group) def save_related_pks_before_group_delete(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] + instance._saved_inventory_pk = instance.inventory.pk instance._saved_parents_pks = set(instance.parents.values_list('pk', flat=True)) instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True)) instance._saved_children_pks = set(instance.children.values_list('pk', flat=True)) @receiver(post_delete, sender=Group) def migrate_children_from_deleted_group_to_parent_groups(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] parents_pks = getattr(instance, '_saved_parents_pks', []) hosts_pks = getattr(instance, '_saved_hosts_pks', []) children_pks = getattr(instance, '_saved_children_pks', []) - for parent_group in Group.objects.filter(pk__in=parents_pks): - for child_host in Host.objects.filter(pk__in=hosts_pks): - logger.debug('adding host %s to parent %s after group deletion', - child_host, parent_group) - parent_group.hosts.add(child_host) - for child_group in Group.objects.filter(pk__in=children_pks): - logger.debug('adding group %s to parent %s after group deletion', - child_group, parent_group) - parent_group.children.add(child_group) + with ignore_inventory_group_removal(): + with ignore_inventory_computed_fields(): + if parents_pks: + for parent_group in Group.objects.filter(pk__in=parents_pks, active=True): + for child_host in Host.objects.filter(pk__in=hosts_pks, active=True): + logger.debug('adding host %s to parent %s after group deletion', + child_host, parent_group) + parent_group.hosts.add(child_host) + for child_group in Group.objects.filter(pk__in=children_pks, active=True): + logger.debug('adding group %s to parent %s after group deletion', + child_group, parent_group) + parent_group.children.add(child_group) + inventory_pk = getattr(instance, '_saved_inventory_pk', None) + if inventory_pk: + try: + inventory = Inventory.objects.get(pk=inventory_pk, active=True) + inventory.update_computed_fields() + except Inventory.DoesNotExist: + pass @receiver(pre_save, sender=Group) def save_related_pks_before_group_marked_inactive(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] if not instance.pk or instance.active: return + instance._saved_inventory_pk = instance.inventory.pk instance._saved_parents_pks = set(instance.parents.values_list('pk', flat=True)) instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True)) instance._saved_children_pks = set(instance.children.values_list('pk', flat=True)) @@ -127,26 +158,41 @@ def save_related_pks_before_group_marked_inactive(sender, **kwargs): @receiver(post_save, sender=Group) def migrate_children_from_inactive_group_to_parent_groups(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] if instance.active: return parents_pks = getattr(instance, '_saved_parents_pks', []) hosts_pks = getattr(instance, '_saved_hosts_pks', []) children_pks = getattr(instance, '_saved_children_pks', []) - for parent_group in Group.objects.filter(pk__in=parents_pks): - for child_host in Host.objects.filter(pk__in=hosts_pks): - logger.debug('moving host %s to parent %s after marking group %s inactive', - child_host, parent_group, instance) - parent_group.hosts.add(child_host) - for child_group in Group.objects.filter(pk__in=children_pks): - logger.debug('moving group %s to parent %s after marking group %s inactive', - child_group, parent_group, instance) - parent_group.children.add(child_group) - parent_group.children.remove(instance) - inventory_source_pk = getattr(instance, '_saved_inventory_source_pk', None) - if inventory_source_pk: - inventory_source = InventorySource.objects.get(pk=inventory_source_pk) - inventory_source.mark_inactive() + with ignore_inventory_group_removal(): + with ignore_inventory_computed_fields(): + if parents_pks: + for parent_group in Group.objects.filter(pk__in=parents_pks, active=True): + for child_host in Host.objects.filter(pk__in=hosts_pks, active=True): + logger.debug('moving host %s to parent %s after marking group %s inactive', + child_host, parent_group, instance) + parent_group.hosts.add(child_host) + for child_group in Group.objects.filter(pk__in=children_pks, active=True): + logger.debug('moving group %s to parent %s after marking group %s inactive', + child_group, parent_group, instance) + parent_group.children.add(child_group) + parent_group.children.remove(instance) + inventory_source_pk = getattr(instance, '_saved_inventory_source_pk', None) + if inventory_source_pk: + try: + inventory_source = InventorySource.objects.get(pk=inventory_source_pk, active=True) + inventory_source.mark_inactive() + except InventorySource.DoesNotExist: + pass + inventory_pk = getattr(instance, '_saved_inventory_pk', None) + if inventory_pk: + try: + inventory = Inventory.objects.get(pk=inventory_pk, active=True) + inventory.update_computed_fields() + except Inventory.DoesNotExist: + pass # Update host pointers to last_job and last_job_host_summary when a job is # marked inactive or deleted. From cb8ec978de7d88edaebcf7e17d0b41c3f805350f Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 01:02:34 -0500 Subject: [PATCH 02/22] AC-983 long group names, AC-970 don't hightlight required fields in red, AC-966 fixed ansibleworks links to point to ansible, AC-733 new pagination widget, page and total rows. --- awx/ui/static/js/app.js | 3 +- awx/ui/static/js/controllers/Credentials.js | 4 +- awx/ui/static/js/controllers/Home.js | 6 +- awx/ui/static/js/controllers/Inventories.js | 9 +- awx/ui/static/js/controllers/JobTemplates.js | 4 +- awx/ui/static/js/controllers/Organizations.js | 4 +- awx/ui/static/js/controllers/Projects.js | 4 +- awx/ui/static/js/controllers/Teams.js | 4 +- awx/ui/static/js/controllers/Users.js | 4 +- awx/ui/static/js/helpers/Groups.js | 2 +- awx/ui/static/js/helpers/Hosts.js | 6 +- awx/ui/static/js/helpers/Lookup.js | 2 +- awx/ui/static/js/helpers/PaginationHelpers.js | 169 ++++++++++++++++++ awx/ui/static/js/helpers/inventory.js | 6 +- awx/ui/static/js/helpers/paginate.js | 82 --------- awx/ui/static/js/helpers/refresh-related.js | 18 +- awx/ui/static/js/helpers/refresh.js | 16 +- awx/ui/static/js/helpers/related-paginate.js | 67 ------- awx/ui/static/js/helpers/search.js | 147 ++++----------- awx/ui/static/js/helpers/teams.js | 2 +- awx/ui/static/js/lists/InventoryGroups.js | 5 +- awx/ui/static/js/lists/InventoryHosts.js | 6 - awx/ui/static/js/widgets/Stream.js | 41 +++-- awx/ui/static/less/ansible-ui.less | 41 ++++- awx/ui/static/lib/ansible/InventoryTree.js | 1 - awx/ui/static/lib/ansible/Utilities.js | 46 ++++- awx/ui/static/lib/ansible/form-generator.js | 16 +- .../static/lib/ansible/generator-helpers.js | 99 ++++------ awx/ui/static/lib/ansible/list-generator.js | 6 +- awx/ui/templates/ui/index.html | 3 +- 30 files changed, 397 insertions(+), 426 deletions(-) create mode 100644 awx/ui/static/js/helpers/PaginationHelpers.js delete mode 100644 awx/ui/static/js/helpers/paginate.js delete mode 100644 awx/ui/static/js/helpers/related-paginate.js diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c636181275..51937f7bca 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -21,9 +21,8 @@ angular.module('ansible', [ 'PromptDialog', 'ApiLoader', 'RelatedSearchHelper', - 'RelatedPaginateHelper', 'SearchHelper', - 'PaginateHelper', + 'PaginationHelpers', 'RefreshHelper', 'AdminListDefinition', 'AWDirectives', diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 21404ec788..a8bbedc751 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -82,7 +82,7 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addCredential = function() { $location.path($location.path() + '/add'); @@ -406,7 +406,7 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP callback: 'choicesReadyCredential' }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); FormSave({ scope: scope, mode: 'edit' }) }; diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index dd2117585a..90f37d786a 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -69,7 +69,7 @@ function Home ($scope, $compile, $routeParams, $rootScope, $location, Wait, Obje ObjectCount({ scope: $scope, target: 'container3', dashboard: data}); }); - $scope.showActivity = function() { Stream(); } + $scope.showActivity = function() { Stream({ scope: $scope }); } $scope.refresh = function() { Wait('start'); @@ -218,7 +218,7 @@ function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, Proce LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.editGroup = function(group_id, inventory_id) { GroupsEdit({ scope: scope, group_id: group_id, inventory_id: inventory_id, groups_reload: false }); @@ -328,7 +328,7 @@ function HomeHosts ($location, $routeParams, HomeHostList, GenerateList, Process LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.toggle_host_enabled = function(id, sources) { ToggleHostEnabled({ host_id: id, external_source: sources, scope: scope }); } diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 5f4c36bf3c..6547d9565c 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -129,7 +129,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res } }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addInventory = function() { $location.path($location.path() + '/add'); @@ -500,15 +500,10 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis $scope.toggleHostEnabled = function(host_id, external_source) { ToggleHostEnabled({ scope: $scope, host_id: host_id, external_source: external_source }); } - - $scope.showHostActivity = function() { - var url = GetBasePath('activity_stream') + '?host__inventory__id=' + $scope.inventory_id; - Stream({ inventory_name: $scope.inventory_name, url: url }); - } $scope.showGroupActivity = function() { var url = GetBasePath('activity_stream') + '?group__inventory__id=' + $scope.inventory_id; - Stream({ inventory_name: $scope.inventory_name, url: url }); + Stream({ scope: $scope, inventory_name: $scope.inventory_name, url: url }); } $scope.showJobSummary = function(job_id) { diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index dd9ba5bd91..fe10b9cdf6 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -48,7 +48,7 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addJobTemplate = function() { $location.path($location.path() + '/add'); @@ -646,7 +646,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route } }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Cancel scope.formReset = function() { diff --git a/awx/ui/static/js/controllers/Organizations.js b/awx/ui/static/js/controllers/Organizations.js index ae940fa77e..6e78ccd111 100644 --- a/awx/ui/static/js/controllers/Organizations.js +++ b/awx/ui/static/js/controllers/Organizations.js @@ -45,7 +45,7 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R PaginateInit({ scope: scope, list: list, url: defaultUrl }); scope.search(list.iterator); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addOrganization = function() { $location.path($location.path() + '/add'); @@ -220,7 +220,7 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Reset the form scope.formReset = function() { diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index e23623d3f7..b6f3a21f16 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -160,7 +160,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addProject = function() { $location.path($location.path() + '/add'); @@ -642,7 +642,7 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Related set: Add button scope.add = function(set) { diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index c81d04121c..261a167f64 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -45,7 +45,7 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addTeam = function() { $location.path($location.path() + '/add'); @@ -237,7 +237,7 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id + '. GET status: ' + status }); }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Save changes to the parent scope.formSave = function() { diff --git a/awx/ui/static/js/controllers/Users.js b/awx/ui/static/js/controllers/Users.js index 8f37b1c07f..50b157a2f9 100644 --- a/awx/ui/static/js/controllers/Users.js +++ b/awx/ui/static/js/controllers/Users.js @@ -45,7 +45,7 @@ function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addUser = function() { $location.path($location.path() + '/add'); @@ -300,7 +300,7 @@ function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Cancel scope.formReset = function() { diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 2da171e82a..67322459ab 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -10,7 +10,7 @@ 'use strict'; angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'GroupListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', 'GroupsHelper', + 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', 'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'InventorySummaryHelpDefinition', 'CredentialsListDefinition', 'InventoryTree' diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index d99e9eba5c..74df35b788 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -8,9 +8,9 @@ */ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'HostListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', 'HostsHelper', - 'InventoryHelper', 'RelatedSearchHelper','RelatedPaginateHelper', - 'InventoryFormDefinition', 'SelectionHelper', 'HostGroupsFormDefinition' + 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'HostsHelper', + 'InventoryHelper', 'RelatedSearchHelper', 'InventoryFormDefinition', 'SelectionHelper', + 'HostGroupsFormDefinition' ]) diff --git a/awx/ui/static/js/helpers/Lookup.js b/awx/ui/static/js/helpers/Lookup.js index ac0dd7a203..5d3dfc4aab 100644 --- a/awx/ui/static/js/helpers/Lookup.js +++ b/awx/ui/static/js/helpers/Lookup.js @@ -14,7 +14,7 @@ * }) */ -angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'ApiLoader' ]) +angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'ApiLoader' ]) .factory('LookUpInit', ['Alert', 'Rest', 'GenerateList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'FormatDate', 'Empty', function(Alert, Rest, GenerateList, SearchInit, PaginateInit, GetBasePath, FormatDate, Empty) { return function(params) { diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js new file mode 100644 index 0000000000..bcab66394c --- /dev/null +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -0,0 +1,169 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * PaginationHelpers.js + * + */ + +angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelatedHelper']) + + .factory('PageRangeSetup', ['Empty', function(Empty) { + return function(params) { + + var scope = params.scope; + var count = params.count; + var next = params.next; + var previous = params.previous; + var iterator = params.iterator; + + scope[iterator + '_page'] = 1; + scope[iterator + '_num_pages'] = Math.ceil((count / scope[iterator + '_page_size'])); + scope[iterator + '_num_pages'] = (scope[iterator + '_num_pages'] <= 0) ? 1 : scope[iterator + '_num_pages']; + scope[iterator + '_total_rows'] = count; + + // Which page are we on? + if ( Empty(next) && previous ) { + // no next page, but there is a previous page + scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/,'')) + 1; + } + else if ( next && Empty(previous) ) { + // next page available, but no previous page + scope[iterator + '_page'] = 1; + } + else if ( next && previous ) { + // we're in between next and previous + scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/,'')) + 1; + } + + // Calc the range of up to 10 pages to show + scope[iterator + '_page_range'] = new Array(); + var first = (scope[iterator + '_page'] > 5) ? scope[iterator + '_page'] - 5 : 1; + if (scope[iterator + '_page'] < 6) { + var last = (10 <= scope[iterator + '_num_pages']) ? 10 : scope[iterator + '_num_pages']; + } + else { + var last = (scope[iterator + '_page'] + 4 < scope[iterator + '_num_pages']) ? + scope[iterator + '_page'] + 4 : scope[iterator + '_num_pages']; + } + for (var i=first; i <= last; i++) { + scope[iterator + '_page_range'].push(i); + } + + } + }]) + + .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', + function(RefreshRelated, $cookieStore) { + return function(params) { + + var scope = params.scope; + var relatedSets = params.relatedSets; + var pageSize = (params.pageSize) ? params.pageSize : 10; + + for (var key in relatedSets){ + cookieSize = $cookieStore.get(relatedSets[key].iterator + '_page_size'); + scope[relatedSets[key].iterator + '_url'] = relatedSets[key].url; + if (cookieSize) { + // use the size found in session cookie, when available + scope[relatedSets[key].iterator + '_page_size'] = cookieSize; + } + else { + scope[relatedSets[key].iterator + '_page'] = 0; + scope[relatedSets[key].iterator + '_page_size'] = pageSize; + } + } + + scope.getPage = function(page, set, iterator) { + var new_url = scope[iterator + '_url'].replace(/.page\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += connect + 'page=' + page; + new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; + Wait('start'); + RefreshRefresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + scope.pageIsActive = function(page, iterator) { + return (page == scope[iterator + '_page']) ? 'active' : ''; + } + + scope.changePageSize = function(set, iterator) { + // Called when a new page size is selected + + scope[iterator + '_page'] = 1; + var url = scope[iterator + '_url']; + + // Using the session cookie, keep track of user rows per page selection + $cookieStore.put(iterator + '_page_size', scope[iterator + '_page_size']); + + url = url.replace(/\/\?.*$/,'/'); + url += (scope[iterator + 'SearchParams']) ? '?' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : + '?page_size=' + scope[iterator + '_page_size' ]; + + RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url }); + } + + } + }]) + + + .factory('PaginateInit', [ 'Refresh', '$cookieStore', 'Wait', + function(Refresh, $cookieStore, Wait) { + return function(params) { + + var scope = params.scope; + var list = params.list; + var iterator = (params.iterator) ? params.iterator : list.iterator; + var mode = (params.mode) ? params.mode : null; + var cookieSize = $cookieStore.get(iterator + '_page_size'); + + scope[iterator + '_page'] = (params.page) ? params.page : 1; + scope[iterator + '_url'] = params.url; + scope[iterator + '_mode'] = mode; + + // Set the default page size + if (cookieSize && mode != 'lookup') { + // use the size found in session cookie, when available + scope[iterator + '_page_size'] = cookieSize; + } + else { + if (params.pageSize) { + scope[iterator + '_page_size'] = params.pageSize; + } + else if (mode == 'lookup') { + scope[iterator + '_page_size'] = 5; + } + else { + scope[iterator + '_page_size'] = 20; + } + } + + scope.getPage = function(page, set, iterator) { + var new_url = scope[iterator + '_url'].replace(/.page\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += connect + 'page=' + page; + new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; + Wait('start'); + Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + scope.pageIsActive = function(page, iterator) { + return (page == scope[iterator + '_page']) ? 'active' : ''; + } + + scope.changePageSize = function(set, iterator) { + // Called whenever a new page size is selected + // Using the session cookie, keep track of user rows per page selection + $cookieStore.put(iterator + '_page_size', scope[iterator + '_page_size']); + scope[iterator + '_page'] = 0; + var new_url = scope[iterator + '_url'].replace(/\?page_size\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += (scope[iterator + 'SearchParams']) ? connect + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : + connect + 'page_size=' + scope[iterator + '_page_size' ]; + Wait('start'); + Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + } + }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index a335d6432a..30258b9c05 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -8,10 +8,8 @@ * */ -angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', - 'InventoryHelper', 'RelatedSearchHelper', 'RelatedPaginateHelper', - 'InventoryFormDefinition', 'ParseHelper' +angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', 'ListGenerator', 'AuthService', + 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper' ]) .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', diff --git a/awx/ui/static/js/helpers/paginate.js b/awx/ui/static/js/helpers/paginate.js deleted file mode 100644 index 7ef1098b89..0000000000 --- a/awx/ui/static/js/helpers/paginate.js +++ /dev/null @@ -1,82 +0,0 @@ -/********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. - * - * PaginateHelper - * - * All the parts for controlling the search widget on - * related collections. - * - * PaginateInit({ - * scope: , - * list:
- * url: < - * }); - * - */ - -angular.module('PaginateHelper', ['RefreshHelper', 'ngCookies', 'Utilities']) - .factory('PaginateInit', [ 'Refresh', '$cookieStore', 'Wait', function(Refresh, $cookieStore, Wait) { - return function(params) { - - var scope = params.scope; - var list = params.list; - var iterator = (params.iterator) ? params.iterator : list.iterator; - var url = params.url; - var mode = (params.mode) ? params.mode : null; - var cookieSize = $cookieStore.get(iterator + 'PageSize'); - - if (params.page) { - scope[iterator + 'Page'] = params.page; - } - else { - scope[iterator + 'Page'] = 0; - } - - if (cookieSize && mode != 'lookup') { - // use the size found in session cookie, when available - scope[iterator + 'PageSize'] = cookieSize; - } - else { - if (params.pageSize) { - scope[iterator + 'PageSize'] = params.pageSize; - } - else if (mode == 'lookup') { - scope[iterator + 'PageSize'] = 5; - } - else { - scope[iterator + 'PageSize'] = 20; - } - } - - scope.nextSet = function(set, iterator) { - if (scope[iterator + 'NextUrl']) { - scope[iterator + 'Page']++; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'NextUrl'] }); - } - }; - - scope.prevSet = function(set, iterator) { - if (scope[iterator + 'PrevUrl']) { - scope[iterator + 'Page']--; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'PrevUrl'] }); - } - }; - - scope.changePageSize = function(set, iterator) { - // Called whenever a new page size is selected - - // Using the session cookie, keep track of user rows per page selection - $cookieStore.put(iterator + 'PageSize', scope[iterator + 'PageSize']); - - scope[iterator + 'Page'] = 0; - var new_url = url.replace(/\?page_size\=\d+/,''); - var connect = (/\/$/.test(new_url)) ? '?' : '&'; - new_url += (scope[iterator + 'SearchParams']) ? connect + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + 'PageSize' ] : - connect + 'page_size=' + scope[iterator + 'PageSize' ]; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); - } - } - }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/refresh-related.js b/awx/ui/static/js/helpers/refresh-related.js index 47c5932325..7a53bd9d08 100644 --- a/awx/ui/static/js/helpers/refresh-related.js +++ b/awx/ui/static/js/helpers/refresh-related.js @@ -14,8 +14,9 @@ * */ -angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities']) - .factory('RefreshRelated', ['ProcessErrors', 'Rest', 'Wait', function(ProcessErrors, Rest, Wait) { +angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities', 'PaginationHelpers']) + .factory('RefreshRelated', ['ProcessErrors', 'Rest', 'Wait', 'PageRangeSetup', + function(ProcessErrors, Rest, Wait, PageRangeSetup) { return function(params) { var scope = params.scope; @@ -26,22 +27,15 @@ angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities']) Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); + PageRangeSetup({ scope: scope, count: data.count, next: data.next, previous: data.previous, iterator: iterator }); scope[set] = data['results']; - scope[iterator + 'NextUrl'] = data.next; - scope[iterator + 'PrevUrl'] = data.previous; - scope[iterator + 'Count'] = data.count; - scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); - //scope[iterator + 'SearchSpin'] = false; scope[iterator + 'Loading'] = false; scope[iterator + 'HoldInput'] = false; + Wait('stop'); scope.$emit('related' + set); - if (!params.scope.$$phase) { - params.scope.$digest(); - } + }) .error ( function(data, status, headers, config) { - //scope[iterator + 'SearchSpin'] = true; ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status }); }); diff --git a/awx/ui/static/js/helpers/refresh.js b/awx/ui/static/js/helpers/refresh.js index 19170d70e7..1efeb9fe07 100644 --- a/awx/ui/static/js/helpers/refresh.js +++ b/awx/ui/static/js/helpers/refresh.js @@ -14,34 +14,32 @@ * */ -angular.module('RefreshHelper', ['RestServices', 'Utilities']) - .factory('Refresh', ['ProcessErrors', 'Rest', 'Wait', function(ProcessErrors, Rest, Wait) { +angular.module('RefreshHelper', ['RestServices', 'Utilities', 'PaginationHelpers']) + .factory('Refresh', ['ProcessErrors', 'Rest', 'Wait', 'Empty', 'PageRangeSetup', + function(ProcessErrors, Rest, Wait, Empty, PageRangeSetup) { return function(params) { var scope = params.scope; var set = params.set; var iterator = params.iterator; var url = params.url; + scope.current_url = url; Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); - scope[iterator + 'NextUrl'] = data.next; - scope[iterator + 'PrevUrl'] = data.previous; - scope[iterator + 'Count'] = data.count; - scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); - //scope[iterator + 'SearchSpin'] = false; + PageRangeSetup({ scope: scope, count: data.count, next: data.next, previous: data.previous, iterator: iterator }); scope[iterator + 'Loading'] = false; for (var i=1; i <= 3; i++) { var modifier = (i == 1) ? '' : i; scope[iterator + 'HoldInput' + modifier] = false; } scope[set] = data['results']; + window.scrollTo(0,0); + Wait('stop'); scope.$emit('PostRefresh'); }) .error ( function(data, status, headers, config) { - //scope[iterator + 'SearchSpin'] = false; scope[iterator + 'HoldInput'] = false; ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status }); diff --git a/awx/ui/static/js/helpers/related-paginate.js b/awx/ui/static/js/helpers/related-paginate.js deleted file mode 100644 index fa868962b6..0000000000 --- a/awx/ui/static/js/helpers/related-paginate.js +++ /dev/null @@ -1,67 +0,0 @@ -/********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. - * - * RelatedPaginateHelper - * - * All the parts for controlling the search widget on - * related collections. - * - * RelatedPaginateInit({ - * scope: , - * relatedSets: , - * form: - * }); - * - */ - -angular.module('RelatedPaginateHelper', ['RefreshRelatedHelper', 'ngCookies']) - .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', function(RefreshRelated, $cookieStore) { - return function(params) { - - var scope = params.scope; - var relatedSets = params.relatedSets; - var pageSize = (params.pageSize) ? params.pageSize : 10; - - for (var key in relatedSets){ - cookieSize = $cookieStore.get(relatedSets[key].iterator + 'PageSize'); - if (cookieSize) { - // use the size found in session cookie, when available - scope[relatedSets[key].iterator + 'PageSize'] = cookieSize; - } - else { - scope[relatedSets[key].iterator + 'Page'] = 0; - scope[relatedSets[key].iterator + 'PageSize'] = pageSize; - } - } - - scope.nextSet = function(set, iterator) { - scope[iterator + 'Page']++; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'NextUrl'] }); - }; - - scope.prevSet = function(set, iterator) { - scope[iterator + 'Page']--; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'PrevUrl'] }); - }; - - scope.changePageSize = function(set, iterator) { - // Called when a new page size is selected - var url; - scope[iterator + 'Page'] = 0; - for (var key in relatedSets) { - if (key == set) { - url = relatedSets[key].url; - break; - } - } - - // Using the session cookie, keep track of user rows per page selection - $cookieStore.put(iterator + 'PageSize', scope[iterator + 'PageSize']); - - url = url.replace(/\/\?.*$/,'/'); - url += (scope[iterator + 'SearchParams']) ? '?' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + 'PageSize' ] : - '?page_size=' + scope[iterator + 'PageSize' ]; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url }); - } - } - }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 56f6ff4881..422636cf2f 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -16,8 +16,9 @@ */ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) - .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', 'GetBasePath', 'Empty', '$timeout', 'Wait', - function(Alert, Rest, Refresh, $location, GetBasePath, Empty, $timeout, Wait) { + + .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', 'GetBasePath', 'Empty', '$timeout', 'Wait', 'Store', + function(Alert, Rest, Refresh, $location, GetBasePath, Empty, $timeout, Wait, Store) { return function(params) { var scope = params.scope; @@ -25,14 +26,19 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var defaultUrl = params.url; var list = params.list; var iterator = (params.iterator) ? params.iterator : list.iterator; - var sort_order; - var expected_objects=0; - var found_objects=0; - - if (scope.searchTimer) { - $timeout.cancel(scope.searchTimer); - } + var setWidgets = (params.setWidgets == false) ? false : true; + var sort_order, expected_objects=0, found_objects=0; + + var params = { + set: set, + defaultUrl: defaultUrl, + list: list, + iterator: iterator + }; + + Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore + function setDefaults(widget) { // Set default values var modifier = (widget == undefined || widget == 1) ? '' : widget; @@ -119,15 +125,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } - // Set default values for each search widget on the page - var widgets = (list.searchWidgets) ? list.searchWidgets : 1; - for (var i=1; i <= widgets; i++) { - var modifier = (i == 1) ? '' : i; - if ( $('#search-widget-container' + modifier) ) { - setDefaults(i); + if (setWidgets) { + // Set default values for each search widget on the page + var widgets = (list.searchWidgets) ? list.searchWidgets : 1; + for (var i=1; i <= widgets; i++) { + var modifier = (i == 1) ? '' : i; + if ( $('#search-widget-container' + modifier) ) { + setDefaults(i); + } } } - + // Functions to handle search widget changes scope.setSearchField = function(iterator, fld, label, widget) { @@ -223,14 +231,13 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) if (scope.removeDoSearch) { scope.removeDoSearch(); } - scope.removeDoSearch = scope.$on('doSearch', function(e, iterator, page, load, spin) { + scope.removeDoSearch = scope.$on('doSearch', function(e, iterator, page, load) { // // Execute the search // - //scope[iterator + 'SearchSpin'] = (spin == undefined || spin == true) ? true : false; scope[iterator + 'Loading'] = (load == undefined || load == true) ? true : false; var url = defaultUrl; - + //finalize and execute the query scope[iterator + 'Page'] = (page) ? parseInt(page) - 1 : 0; if (scope[iterator + 'SearchParams']) { @@ -242,7 +249,7 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } url = url.replace(/\&\&/,'&'); - url += (scope[iterator + 'PageSize']) ? '&page_size=' + scope[iterator + 'PageSize'] : ""; + url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : ""; if (page) { url += '&page=' + page; } @@ -252,38 +259,7 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) Refresh({ scope: scope, set: set, iterator: iterator, url: url }); }); - /* - if (scope.removeFoundObject) { - scope.removeFoundObject(); - } - scope.removeFoundObject = scope.$on('foundObject', function(e, iterator, page, load, spin, widget, pk) { - found_objects++; - // Add new criteria to search params - var modifier = (widget == 1) ? '' : widget; - var objs = list.fields[scope[iterator + 'SearchField' + modifier]].searchObject; - var o = (objs == 'inventories') ? 'inventory' : objs.replace(/s$/,''); - var searchFld = list.fields[scope[iterator + 'SearchField' + modifier]].searchField; - scope[iterator + 'SearchParams'] += '&' + searchFld + '__icontains=' + o; - if (!Empty(pk)) { - scope[iterator + 'SearchParams'] += '&' + searchFld + '_id__in=' + pk; - } - // Move to the next phase once all object types are processed - if (found_objects == expected_objects) { - scope.$emit('prepareSearch2', iterator, page, load, spin); - } - }); - */ - - /*if (scope.removeResultWarning) { - scope.removeResultWarning(); - } - scope.removeResultWarning = scope.$on('resultWarning', function(e, objs, length) { - // Alert the user that the # of objects was greater than 30 - var label = (objs == 'inventory') ? 'inventories' : objs.replace(/s$/,''); - Alert('Warning', 'The number of matching ' + label + ' was too large. We limited your search to the first 30.', 'alert-info'); - }); - */ - + if (scope.removePrepareSearch) { scope.removePrepareSearch(); } @@ -297,22 +273,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var widgets = (list.searchWidgets) ? list.searchWidgets : 1; var modifier; - // Determine how many object values we're dealing with. - /* - expected_objects = 0; - found_objects = 0; - for (var i=1; i <= widgets; i++) { - modifier = (i == 1) ? '' : i; - scope[iterator + 'HoldInput' + modifier] = true; //Block any input until we're done. Refresh.js will flip this back. - if ($('#search-widget-container' + modifier) && - list.fields[scope[iterator + 'SearchField' + modifier]] && - list.fields[scope[iterator + 'SearchField' + modifier]].searchObject && - list.fields[scope[iterator + 'SearchField' + modifier]].searchObject !== 'all') { - expected_objects++; - } - } - */ - for (var i=1; i <= widgets; i++) { var modifier = (i == 1) ? '' : i; if ( $('#search-widget-container' + modifier) ) { @@ -329,33 +289,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + '__name__icontains=' + scope[iterator + 'SearchValue' + modifier]; - - //var objUrl = GetBasePath('base') + objs + '/?name__icontains=' + scope[iterator + 'SearchValue' + modifier]; - /* - Rest.setUrl(objUrl); - Rest.setHeader({ widget: i }); - Rest.setHeader({ object: objs }); - Rest.get() - .success( function(data, status, headers, config) { - var pk=''; - //limit result set to 30 - var len = (data.results.length > 30) ? 30 : data.results.length; - for (var j=0; j < len; j++) { - pk += "," + data.results[j].id; - } - pk = pk.replace(/^\,/,''); - scope.$emit('foundObject', iterator, page, load, spin, config.headers['widget'], pk); - if (data.results.length > 30) { - scope.$emit('resultWarning', config.headers['object'], data.results.length); - } - }) - .error( function(data, status, headers, config) { - Wait('stop'); - ProcessErrors(scope, data, status, null, - { hdr: 'Error!', msg: 'Retrieving list of ' + objs + ' where name contains: ' + scope[iterator + 'SearchValue' + modifier] + - ' GET returned status: ' + status }); - }); - */ } else { // Search value is empty @@ -363,7 +296,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) scope[iterator + 'SearchParams'] += '&' + list.fields[scope[iterator + 'SearchField' + modifier]].searchField + '=' + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject; - //scope.$emit('foundObject', iterator, page, load, spin, i, null); } } else { @@ -374,10 +306,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } scope.$emit('prepareSearch2', iterator, page, load, spin); - //if (expected_objects == 0) { - // No search widgets contain objects - // scope.$emit('prepareSearch2', iterator, page, load, spin); - //} }); if (scope.removePrepareSearch2) { @@ -437,17 +365,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) scope[iterator + 'SearchSelectValue' + modifier].value == null) ) { scope[iterator + 'SearchParams'] += 'iexact='; } - /*else if ( (list.fields[scope[iterator + 'SearchField' + modifier]].searchType && - (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'or')) ) { - scope[iterator + 'SearchParams'] = ''; //start over - var val = scope[iterator + 'SearchValue' + modifier]; - for (var k=0; k < list.fields[scope[iterator + 'SearchField' + modifier]].searchFields.length; k++) { - scope[iterator + 'SearchParams'] += '&or__' + - list.fields[scope[iterator + 'SearchField' + modifier]].searchFields[k] + - '__icontains=' + escape(val); - } - scope[iterator + 'SearchParams'].replace(/^\&/,''); - }*/ else { scope[iterator + 'SearchParams'] += scope[iterator + 'SearchType' + modifier] + '='; } @@ -491,12 +408,16 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } - scope.search = function(iterator, page, load, spin) { + scope.search = function(iterator, page, load) { // Called to initiate a searh. // Page is optional. Added to accomodate back function on Job Events detail. // Spin optional -set to false if spin not desired. // Load optional -set to false if loading message not desired - scope.$emit('prepareSearch', iterator, page, load, spin); + var load = (load === undefined) ? true : false; + if (load) { + scope[set] = []; + } + scope.$emit('prepareSearch', iterator, page, load); } diff --git a/awx/ui/static/js/helpers/teams.js b/awx/ui/static/js/helpers/teams.js index 00a73fe20b..8bd6e626b0 100644 --- a/awx/ui/static/js/helpers/teams.js +++ b/awx/ui/static/js/helpers/teams.js @@ -6,7 +6,7 @@ */ angular.module('TeamHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator' ]) + 'SearchHelper', 'PaginationHelpers', 'ListGenerator' ]) .factory('SetTeamListeners', ['Alert', 'Rest', function(Alert, Rest) { return function(params) { diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index 5aa136a611..f341e42b69 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -28,7 +28,8 @@ angular.module('InventoryGroupsDefinition', []) ngClick: "\{\{ 'showHosts(' + group.id + ',' + group.group_id + ', false)' \}\}", ngClass: "group.selected_class", hasChildren: true, - columnClass: 'col-lg-9 ellipsis', + columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', + 'class': 'ellipsis', nosort: true, awDroppable: "\{\{ group.isDroppable \}\}", awDraggable: "\{\{ group.isDraggable \}\}", @@ -41,7 +42,7 @@ angular.module('InventoryGroupsDefinition', []) actions: { - columnClass: 'col-lg-3', + columnClass: 'col-lg-3 col-md-3 col-sm-5 col-xs-5', create: { mode: 'all', diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index 1a9760c02f..d59854352b 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -97,12 +97,6 @@ angular.module('InventoryHostsDefinition', []) ngHide: 'selected_tree_id == 1', //disable when 'All Hosts' selected awToolTip: "Create a new host" }, - stream: { - mode: 'all', - ngClick: "showHostActivity()", - awToolTip: "View Activity Stream", - ngShow: "user_is_superuser" - }, help: { mode: 'all', awToolTip: diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index dc2d47ccc2..5fa61f73f2 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -7,15 +7,16 @@ * */ -angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginateHelper', +angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginationHelpers', 'RefreshHelper', 'ListGenerator', 'StreamWidget', 'AuthService']) .factory('setStreamHeight', [ function() { return function() { // Try not to overlap footer. Because stream is positioned absolute, the parent // doesn't resize correctly when stream is loaded. - var stream = $('#stream-container'); - var height = stream.height() + 50; + var sheight = $('#stream-content').height(); + var theight = parseInt($('#tab-content-container').css('min-height').replace(/px/,'')); + var height = (theight < sheight) ? sheight : theight; $('#tab-content-container').css({ "min-height": height }); } }]) @@ -253,16 +254,20 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'BuildUrl', - 'ShowDetail', 'StreamBreadCrumbs', 'setStreamHeight', 'Find', + 'ShowDetail', 'StreamBreadCrumbs', 'setStreamHeight', 'Find', 'Store', function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail, StreamBreadCrumbs, setStreamHeight, - Find) { + Find, Store) { return function(params) { var list = StreamList; var defaultUrl = GetBasePath('activity_stream'); var view = GenerateList; var base = $location.path().replace(/^\//,'').split('/')[0]; + var parent_scope = params.scope; + + // Hang onto current search params + var PreviousSearchParams = Store('SearchInitParams'); // pass in an inventory name to fix breadcrumb display var inventory_name = (params) ? params.inventory_name : null; @@ -324,11 +329,23 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti scope.closeStream = function(inUrl) { HideStream(); - if (scope.searchCleanup) + if (scope.searchCleanup) { scope.searchCleanup(); - if (inUrl) + } + // Restore prior search state + if (PreviousSearchParams) { + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + setWidgets: false }); + } + if (inUrl) { $location.path(inUrl); } + } scope.refreshStream = function() { scope.search(list.iterator); @@ -347,14 +364,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti cDate = new Date(scope['activities'][i].timestamp); scope['activities'][i].timestamp = FormatDate(cDate); - // Display username - /*scope['activities'][i].user = (scope['activities'][i].summary_fields.user) ? scope['activities'][i].summary_fields.user.username : - 'system'; - if (scope['activities'][i].user !== 'system') { - // turn user into a link when not 'system' - scope['activities'][i].user = "" + - scope['activities'][i].user + ""; - }*/ if (scope['activities'][i]['summary_fields']['actor']) { scope['activities'][i]['user'] = "" + scope['activities'][i]['summary_fields']['actor']['username'] + ""; @@ -399,7 +408,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Give ng-repeate a chance to show the data before adjusting the page size. setTimeout(function() { setStreamHeight(); }, 500); }); - + // 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 }); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 1e02a23c0f..0666752e3d 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -116,13 +116,16 @@ a:focus { .actions { a { font-size: 14px; - margin-right: 15px; + margin-right: 12px; + } + a:last-child { + margin-right: 0; } a:hover { cursor: pointer; } .cancel { - padding-right: 10px; + padding-right: 8px; } .dropdown .caret { border-top-color: @blue; @@ -130,6 +133,7 @@ a:focus { } #home_groups_table .actions .cancel { padding-right: 3px; } +#jobs_table .actions .cancel { padding-right: 10px; } .success-badge { color: #ffffff; @@ -534,6 +538,39 @@ legend { font-size: 11px; } +/* Pagination */ + .page-label { + margin-top: 0; + text-align: right; + } + + .pagination { + margin-top: 0; + margin-bottom: 7px; + } + + .pagination > li > a { + padding: 3px 6px; + } + + .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: #fff; + color: #428bca; + border-color: none; + border: 1px solid #428bca; + } + } + .footer-navigation { margin: 10px 0 10px 0; } diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index da621d0a7d..7b31832698 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -136,7 +136,6 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P .success( function(data, status, headers, config) { buildAllHosts(data); buildGroups(data, 0, 0); - //console.log(groups); if (refresh) { scope.groups = groups; scope.$emit('GroupTreeRefreshed', inventory_name, groups, emit); diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 64b8d9e5dd..dfed22aea3 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -140,7 +140,8 @@ angular.module('Utilities',['RestServices', 'Utilities']) if (form.fields[field].realName) { if (data[form.fields[field].realName]) { scope[field + '_api_error'] = data[form.fields[field]][0]; - scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false); + //scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false); + $('[name="' + form.fields[field].realName + '"]').addClass('ng-invalid'); fieldErrors = true; } } @@ -148,15 +149,16 @@ angular.module('Utilities',['RestServices', 'Utilities']) if (data[field]) { scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '_api_error'] = data[field][0]; - scope[form.name + '_form'][form.fields[field].sourceModel + '_' + form.fields[field].sourceField].$setValidity('apiError', false); + //scope[form.name + '_form'][form.fields[field].sourceModel + '_' + form.fields[field].sourceField].$setValidity('apiError', false); + $('[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').addClass('ng-invalid'); fieldErrors = true; } } else { if (data[field]) { - console.log('setting api error: ' + form.name + '_form.' + field); scope[field + '_api_error'] = data[field][0]; - scope[form.name + '_form'][field].$setValidity('apiError', false); + //scope[form.name + '_form'][field].$setValidity('apiError', false); + $('[name="' + field + '"]').addClass('ng-invalid'); fieldErrors = true; } } @@ -523,6 +525,7 @@ angular.module('Utilities',['RestServices', 'Utilities']) } }]) + /* Empty() * * Test if a value is 'empty'. Returns true if val is null | '' | undefined. @@ -533,7 +536,40 @@ angular.module('Utilities',['RestServices', 'Utilities']) return function(val) { return (val === null || val === undefined || val === '') ? true : false; } - }]); + }]) + + + /* Store + * + * Wrapper for local storage. All local storage requests flow through here so that we can + * stringify/unstringify objects and respond to future issues in one place. For example, + * we may at some point want to only use session storage rather than local storage. We might + * want to add a test for whether or not local/session storage exists for the browser, etc. + * + * store(key,value) will store the value using the key + * + * store(key) retrieves the value of the key + * + */ + .factory('Store', ['Empty', function(Empty) { + return function(key, value) { + if (!Empty(value)) { + // Store the value + localStorage[key] = JSON.stringify(value); + } + else if (!Empty(key)) { + // Return the value + var val = localStorage[key]; + return (!Empty(val)) ? JSON.parse(val) : null; + } + } + }]) + + + + + + diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 8c251f586a..375f1e9a99 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -237,11 +237,13 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) if (f.sourceModel) { scope[f.sourceModel + '_' + f.sourceField] = ''; scope[f.sourceModel + '_' + f.sourceField + '_api_error'] = ''; - scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + if (scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField]) { + scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + } } if (f.type == 'lookup' && scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField]) { scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setPristine(); - scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); } if (scope[form.name + '_form'][fld]) { scope[form.name + '_form'][fld].$setPristine(); @@ -361,12 +363,18 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) }, clearApiErrors: function() { - for (fld in this.form.fields) { + for (var fld in this.form.fields) { if (this.form.fields[fld].sourceModel) { this.scope[this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField + '_api_error'] = ''; + $('[name="' + this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField + '"]').removeClass('ng-invalid'); + } + else if (this.form.fields[fld].realName) { + this.scope[this.form.fields[fld].realName + '_api_error'] = ''; + $('[name="' + this.form.fields[fld].realName + '"]').removeClass('ng-invalid'); } else { - this.scope[fld + '_api_error'] = ''; + this.scope[fld + '_api_error'] = ''; + $('[name="' + fld + '"]').removeClass('ng-invalid'); } } if (!this.scope.$$phase) { diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 2c13b87a98..9d29e82882 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -233,7 +233,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) var list = params['list']; var fld = params['fld']; var options = params['options']; - var field; + var html, field; if (params.field) { field = params.field; @@ -256,19 +256,6 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html = ''; } - /* - html += "
\n"; - html += "\n"; - */ html += "\n"; - return html; } }) - - .factory('PaginateWidget', function() { + + .factory('PaginateWidget', [ function() { return function(params) { - var set = params.set; - var iterator = params.iterator; - var useMini = params.mini; - var mode = (params.mode) ? params.mode : null; - var html = ''; - - if (mode == 'lookup') { - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - if (mode != 'lookup') { - html += "\n"; - html += "\n"; - } - - html += "
0\" "; - html += ">Page: {{ " + iterator + "Page + 1 }} of {{ " + iterator + "PageCount }}
\n"; - html += "\n"; + var iterator = params.iterator; + var set = params.set; + var html = ''; + html += "\n"; + html += "
\n"; + html += "
\n"; + html += "\n"; + html += "
\n"; + html += "
\n"; + html += "
\n"; + html += "Page {{ " + iterator + "_page }} of {{ " + iterator + "_num_pages }} for {{ " + iterator + "_total_rows | number:0 }} " + set + '.'; + html += "
\n"; + html += "
\n"; html += "
\n"; return html; - } - }); \ No newline at end of file + }]); + + diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index d8f30762f1..bc0951eafa 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -300,7 +300,7 @@ angular.module('ListGenerator', ['GeneratorHelpers']) html += "\""; html += ">\n"; if (list.index) { - html += "{{ $index + (" + list.iterator + "Page * " + list.iterator + "PageSize) + 1 }}.\n"; + html += "{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.\n"; } var cnt = 2; var base = (list.base) ? list.base : list.name; @@ -391,10 +391,10 @@ angular.module('ListGenerator', ['GeneratorHelpers']) if (list.name !== 'groups') { if ( options.mode == 'lookup' || (options.id && options.id == "form-modal-body") ) { - html += PaginateWidget({ set: list.name, iterator: list.iterator, mini: true, mode: 'lookup' }); + html += PaginateWidget({ set: list.name, iterator: list.iterator }); } else { - html += PaginateWidget({ set: list.name, iterator: list.iterator, mini: true }); + html += PaginateWidget({ set: list.name, iterator: list.iterator }); } } diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 478f1e9c69..1e1ab7be0e 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -95,11 +95,10 @@ - - + From c78728fdf3d40bdd629b9c39075fbaf316f4c13f Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 28 Jan 2014 09:58:44 -0500 Subject: [PATCH 03/22] Allow customizing packer license tier --- Makefile | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d182fa7048..e7ea085bdd 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,11 @@ DATE := $(shell date -u +%Y%m%d%H%M) VERSION=$(shell $(PYTHON) -c "from awx import __version__; print(__version__.split('-')[0])") RELEASE=$(shell $(PYTHON) -c "from awx import __version__; print(__version__.split('-')[1])") + +# Allow ami license customization +LICENSE_TIER ?= 30 +PACKER_LICENSE_FILE ?= test.json + ifneq ($(OFFICIAL),yes) BUILD=dev$(DATE) SDIST_TAR_FILE=awx-$(VERSION)-$(BUILD).tar.gz @@ -15,6 +20,7 @@ SETUP_TAR_NAME=awx-setup-$(VERSION)-$(BUILD) RPM_PKG_RELEASE=$(BUILD) DEB_BUILD_DIR=deb-build/awx-$(VERSION)-$(BUILD) DEB_PKG_RELEASE=$(VERSION)-$(BUILD) +PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-nightly.json else BUILD= SDIST_TAR_FILE=awx-$(VERSION).tar.gz @@ -22,6 +28,7 @@ SETUP_TAR_NAME=awx-setup-$(VERSION) RPM_PKG_RELEASE=$(RELEASE) DEB_BUILD_DIR=deb-build/awx-$(VERSION) DEB_PKG_RELEASE=$(VERSION)-$(RELEASE) +PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-release.json endif .PHONY: clean rebase push requirements requirements_pypi develop refresh \ @@ -188,12 +195,11 @@ deb: sdist @echo "awx_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") -ami: - if [ "$(OFFICIAL)" = "yes" ] ; then \ - (cd packaging/ami && $(PACKER) build -var-file=vars-awxkeys.json -var-file=vars-release.json awx.json) ; \ - else \ - (cd packaging/ami && $(PACKER) build -var-file=vars-awxkeys.json -var-file=vars-nightly.json awx.json) ; \ - fi +packer_license: + @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" + +ami: packer_license + (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) install: $(PYTHON) setup.py install egg_info -b "" From ef52e92c86a7e93b4918e45c3de470bcf3a86e4f Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 28 Jan 2014 10:21:30 -0500 Subject: [PATCH 04/22] Correct incorrect Makefile variable --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e7ea085bdd..6b127cfb41 100644 --- a/Makefile +++ b/Makefile @@ -196,7 +196,7 @@ deb: sdist (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: - @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" + @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE_FILE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" ami: packer_license (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) From de970058f7935fdfde4ae44fb0671ebe58b967e4 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 10:58:50 -0500 Subject: [PATCH 05/22] Fixed inventory group source drop-down so that selecting 'Choose a source' behaves the same as choosing 'Manual'. Fixed AC-989. --- awx/ui/static/js/forms/Groups.js | 12 +- awx/ui/static/js/helpers/Groups.js | 194 ++++++++++---------- awx/ui/static/lib/ansible/form-generator.js | 6 +- 3 files changed, 105 insertions(+), 107 deletions(-) diff --git a/awx/ui/static/js/forms/Groups.js b/awx/ui/static/js/forms/Groups.js index 1a9691dee9..fd912a1de2 100644 --- a/awx/ui/static/js/forms/Groups.js +++ b/awx/ui/static/js/forms/Groups.js @@ -67,12 +67,12 @@ angular.module('GroupFormDefinition', []) ngChange: 'sourceChange()', addRequired: false, editRequired: false, - 'default': { label: 'Manual', value: '' }, + //'default': { label: 'Manual', value: '' }, tab: 'source' }, source_path: { label: 'Script Path', - ngShow: "source.value == 'file'", + ngShow: "source && source.value == 'file'", type: 'text', awRequiredWhen: {variable: "sourcePathRequired", init: "false" }, tab: 'source' @@ -80,7 +80,7 @@ angular.module('GroupFormDefinition', []) credential: { label: 'Cloud Credential', type: 'lookup', - ngShow: "source.value !== ''", + ngShow: "source && source.value !== ''", sourceModel: 'credential', sourceField: 'name', ngClick: 'lookUpCredential()', @@ -91,7 +91,7 @@ angular.module('GroupFormDefinition', []) source_regions: { label: 'Regions', type: 'text', - ngShow: "source.value == 'rax' || source.value == 'ec2'", + ngShow: "source && (source.value == 'rax' || source.value == 'ec2')", addRequired: false, editRequired: false, awMultiselect: 'source_region_choices', @@ -105,7 +105,7 @@ angular.module('GroupFormDefinition', []) }, source_vars: { label: 'Source Variables', - ngShow: "source.value == 'file' || source.value == 'ec2'", + ngShow: "source && (source.value == 'file' || source.value == 'ec2')", type: 'textarea', addRequired: false, editRequird: false, @@ -145,7 +145,7 @@ angular.module('GroupFormDefinition', []) checkbox_group: { label: 'Update Options', type: 'checkbox_group', - ngShow: "source.value !== '' && source.value !== null", + ngShow: "source && (source.value !== '' && source.value !== null)", tab: 'source', fields: [ diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 67322459ab..4624e1eb89 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -27,9 +27,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.options() .success( function(data, status, headers, config) { var choices = data.actions.GET.source.choices + console.log(choices); for (var i=0; i < choices.length; i++) { if (choices[i][0] !== 'file') { - scope[variable].push({ label: (choices[i][0] == "") ? 'Manual' : choices[i][1] , value: choices[i][0] }); + scope[variable].push({ + label: ( (choices[i][0] == '') ? 'Manual' : choices[i][1] ), + value: choices[i][0] + }); } } }) @@ -212,42 +216,46 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } }]) - .factory('SourceChange', [ 'GetBasePath', 'CredentialList', 'LookUpInit', - function(GetBasePath, CredentialList, LookUpInit){ + .factory('SourceChange', [ 'GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', + function(GetBasePath, CredentialList, LookUpInit, Empty){ return function(params) { var scope = params.scope; - var form = params.form; - - if (scope['source'].value == 'file') { - scope.sourcePathRequired = true; + var form = params.form; + + if (!Empty(scope['source'])) { + if (scope['source'].value == 'file') { + scope.sourcePathRequired = true; + } + else { + scope.sourcePathRequired = false; + // reset fields + scope.source_path = ''; + scope[form.name + '_form']['source_path'].$setValidity('required',true); + } + if (scope['source'].value == 'rax') { + scope['source_region_choices'] = scope['rax_regions']; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); + } + else if (scope['source'].value == 'ec2') { + scope['source_region_choices'] = scope['ec2_regions']; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); + } + if (scope['source'].value == 'rax' || scope['source'].value == 'ec2') { + var kind = (scope.source.value == 'rax') ? 'rax' : 'aws'; + var url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; + LookUpInit({ + url: url, + scope: scope, + form: form, + list: CredentialList, + field: 'credential' + }); + } } - else { - scope.sourcePathRequired = false; - // reset fields - scope.source_path = ''; - scope[form.name + '_form']['source_path'].$setValidity('required',true); - } - if (scope['source'].value == 'rax') { - scope['source_region_choices'] = scope['rax_regions']; - //$('#s2id_group_source_regions').select2('data', []); - $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); - } - else if (scope['source'].value == 'ec2') { - scope['source_region_choices'] = scope['ec2_regions']; - //$('#s2id_group_source_regions').select2('data', []); - $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); - } - else - var kind = (scope.source.value == 'rax') ? 'rax' : 'aws'; - var url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; - LookUpInit({ - url: url, - scope: scope, - form: form, - list: CredentialList, - field: 'credential' - }); + } }]) @@ -862,75 +870,65 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' var parseError = false; var saveError = false; - // Update the selector tree with new group name, descr - //SetNodeName({ scope: scope['selectedNode'], group_id: group_id, - // name: scope.name, description: scope.description }); + var data = { group: group_id, + source: ( (source && source.value) ? source.value : '' ), + source_path: scope['source_path'], + credential: scope['credential'], + overwrite: scope['overwrite'], + overwrite_vars: scope['overwrite_vars'], + update_on_launch: scope['update_on_launch'] + //update_interval: scope['update_interval'].value + }; - if (scope.source.value !== null && scope.source.value !== '') { - var data = { group: group_id, - source: scope['source'].value, - source_path: scope['source_path'], - credential: scope['credential'], - overwrite: scope['overwrite'], - overwrite_vars: scope['overwrite_vars'], - update_on_launch: scope['update_on_launch'] - //update_interval: scope['update_interval'].value - }; - - // Create a string out of selected list of regions - var regions = $('#s2id_group_source_regions').select2("data"); - var r = []; - for (var i=0; i < regions.length; i++) { - r.push(regions[i].id); + // Create a string out of selected list of regions + var regions = $('#s2id_group_source_regions').select2("data"); + var r = []; + for (var i=0; i < regions.length; i++) { + r.push(regions[i].id); + } + data['source_regions'] = r.join(); + + if (scope['source'].value == 'ec2') { + // for ec2, validate variable data + try { + if (scope.envParseType == 'json') { + var json_data = JSON.parse(scope.source_vars); //make sure JSON parses + } + else { + var json_data = jsyaml.load(scope.source_vars); //parse yaml + } + + // Make sure our JSON is actually an object + if (typeof json_data !== 'object') { + throw "failed to return an object!"; + } + + // Send JSON as a string + if ($.isEmptyObject(json_data)) { + data.source_vars = ""; + } + else { + data.source_vars = JSON.stringify(json_data, undefined, '\t'); + } } - data['source_regions'] = r.join(); - - if (scope['source'].value == 'ec2') { - // for ec2, validate variable data - try { - if (scope.envParseType == 'json') { - var json_data = JSON.parse(scope.source_vars); //make sure JSON parses - } - else { - var json_data = jsyaml.load(scope.source_vars); //parse yaml - } - - // Make sure our JSON is actually an object - if (typeof json_data !== 'object') { - throw "failed to return an object!"; - } - - // Send JSON as a string - if ($.isEmptyObject(json_data)) { - data.source_vars = ""; - } - else { - data.source_vars = JSON.stringify(json_data, undefined, '\t'); - } - } - catch(err) { - parseError = true; - scope.$emit('SaveComplete', true); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); - } - } - - if (!parseError) { - Rest.setUrl(scope.source_url) - Rest.put(data) - .success( function(data, status, headers, config) { - scope.$emit('SaveComplete', false); - }) - .error( function(data, status, headers, config) { - scope.$emit('SaveComplete', true); - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to update group inventory source. PUT status: ' + status }); - }); + catch(err) { + parseError = true; + scope.$emit('SaveComplete', true); + Alert("Error", "Error parsing extra variables. Parser returned: " + err); } } - else { - // No source value - scope.$emit('SaveComplete', false); + + if (!parseError) { + Rest.setUrl(scope.source_url) + Rest.put(data) + .success( function(data, status, headers, config) { + scope.$emit('SaveComplete', false); + }) + .error( function(data, status, headers, config) { + scope.$emit('SaveComplete', true); + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update group inventory source. PUT status: ' + status }); + }); } }); diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 375f1e9a99..780aae9e99 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -735,7 +735,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) field.awRequiredWhen.variable + "\" " : ""; html += ">\n"; html += "\n"; html += "\n"; // Add error messages @@ -1462,8 +1462,8 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) html += "\n"; if (form.related[itm].index == undefined || form.related[itm].index !== false) { - html += "{{ $index + (" + form.related[itm].iterator + "Page * " + - form.related[itm].iterator + "PageSize) + 1 }}.\n"; + html += "{{ $index + ((" + form.related[itm].iterator + "_page - 1) * " + + form.related[itm].iterator + "_page_size) + 1 }}.\n"; } var cnt = 1; var rfield; From 77b285f632487279f51fe4e5e85ae18834349243 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 13:57:37 -0500 Subject: [PATCH 06/22] AC-941 don't display UI form fields until API data loads. Also fixed related set pagination issue. --- awx/ui/static/js/controllers/JobTemplates.js | 121 ++++++++++-------- awx/ui/static/js/controllers/Jobs.js | 50 ++++---- awx/ui/static/js/controllers/Teams.js | 2 +- awx/ui/static/js/forms/JobTemplates.js | 2 +- awx/ui/static/js/helpers/PaginationHelpers.js | 6 +- 5 files changed, 97 insertions(+), 84 deletions(-) diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index fe10b9cdf6..0fb999e30c 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -180,7 +180,6 @@ function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeP Wait('stop'); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get playbook list for ' + url +'. GET returned status: ' + status }); }); @@ -323,6 +322,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route var generator = GenerateForm; var form = JobTemplateForm; var scope = generator.inject(form, {mode: 'edit', related: true}); + var loadingFinishedCount = 0; scope.parseType = 'yaml'; ParseTypeChange(scope); @@ -354,12 +354,14 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route for (var i=0; i < data.length; i++) { scope.playbook_options.push(data[i]); if (data[i] == scope.playbook) { - scope['job_templates_form']['playbook'].$setValidity('required',true); + scope['job_templates_form']['playbook'].$setValidity('required',true); } } - Wait('stop'); - if (!scope.$$phase) { - scope.$digest(); + if (scope.playbook) { + scope.$emit('jobTemplateLoadFinished'); + } + else { + Wait('stop'); } }) .error( function(data, status, headers, config) { @@ -368,14 +370,12 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route ' project or make the playbooks available on the file system.', 'alert-info'); }); } - else { - Wait('stop'); - } } // Detect and alert user to potential SCM status issues var checkSCMStatus = function() { - if (!Empty(scope.project)) { + if (!Empty(scope.project)) { + Wait('start'); Rest.setUrl(GetBasePath('projects') + scope.project + '/'); Rest.get() .success( function(data, status, headers, config) { @@ -395,7 +395,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route ' the directory exists and file permissions are set correctly.'; break; } - + Wait('stop'); if (msg) { Alert('Waning', msg, 'alert-info'); } @@ -409,10 +409,10 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route // Register a watcher on project_name. Refresh the playbook list on change. - if (scope.selectPlaybookUnregister) { - scope.selectPlaybookUnregister(); + if (scope.watchProjectUnregister) { + scope.watchProjectUnregister(); } - scope.selectPlaybookUnregister = scope.$watch('project_name', function(oldValue, newValue) { + scope.watchProjectUnregister = scope.$watch('project_name', function(oldValue, newValue) { if (oldValue !== newValue && newValue !== '' && newValue !== null && newValue !== undefined) { scope.playbook = null; getPlaybooks(scope.project); @@ -420,6 +420,22 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route } }); + // Turn off 'Wait' after both cloud credential and playbook list come back + if (scope.removeJobTemplateLoadFinished) { + scope.removeJobTemplateLoadFinished(); + } + scope.removeJobTemplateLoadFinished = scope.$on('jobTemplateLoadFinished', function() { + loadingFinishedCount++; + if (loadingFinishedCount >= 2) { + // The initial template load finished. Now load related jobs, which + // will turn off the 'working' spinner. + for (var set in relatedSets) { + scope.search(relatedSets[set].iterator); + } + + } + }); + // Set the status/badge for each related job if (scope.removeRelatedJobs) { scope.removeRelatedJobs(); @@ -460,19 +476,18 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route field: 'cloud_credential', hdr: 'Select Cloud Credential' }); + scope.$emit('jobTemplateLoadFinished'); }); + // Retrieve each related set and populate the playbook list if (scope.jobTemplateLoadedRemove) { scope.jobTemplateLoadedRemove(); } scope.jobTemplateLoadedRemove = scope.$on('jobTemplateLoaded', function(e, related_cloud_credential) { - for (var set in relatedSets) { - scope.search(relatedSets[set].iterator); - } - getPlaybooks(scope.project); - //$('#forks-slider').slider('value',scope.forks); // align slider handle with value. + getPlaybooks(scope.project); + var dft = (scope['host_config_key'] === "" || scope['host_config_key'] === null) ? 'false' : 'true'; md5Setup({ scope: scope, @@ -488,7 +503,6 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route scope.$emit('cloudCredentialReady', data.name); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to related cloud credential. GET returned status: ' + status }); }); @@ -506,48 +520,49 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route .success( function(data, status, headers, config) { LoadBreadCrumbs({ path: '/job_templates/' + id, title: data.name }); for (var fld in form.fields) { - if (fld != 'variables' && data[fld] !== null && data[fld] !== undefined) { - if (form.fields[fld].type == 'select') { - if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { - for (var i=0; i < scope[fld + '_options'].length; i++) { - if (data[fld] == scope[fld + '_options'][i].value) { - scope[fld] = scope[fld + '_options'][i]; + if (fld != 'variables' && data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type == 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (var i=0; i < scope[fld + '_options'].length; i++) { + if (data[fld] == scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } } } - } - else { + else { + scope[fld] = data[fld]; + } + } + else { scope[fld] = data[fld]; - } - } - else { - scope[fld] = data[fld]; - } - master[fld] = scope[fld]; - } - if (fld == 'variables') { - // Parse extra_vars, converting to YAML. - if ($.isEmptyObject(data.extra_vars) || data.extra_vars == "\{\}" || data.extra_vars == "null" - || data.extra_vars == "" || data.extra_vars == null) { - scope.variables = "---"; - } - else { - var json_obj = JSON.parse(data.extra_vars); - scope.variables = jsyaml.safeDump(json_obj); - } - master.variables = scope.variables; - } - if (form.fields[fld].type == 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + } + master[fld] = scope[fld]; + } + if (fld == 'variables') { + // Parse extra_vars, converting to YAML. + if ($.isEmptyObject(data.extra_vars) || data.extra_vars == "\{\}" || data.extra_vars == "null" + || data.extra_vars == "" || data.extra_vars == null) { + scope.variables = "---"; + } + else { + var json_obj = JSON.parse(data.extra_vars); + scope.variables = jsyaml.safeDump(json_obj); + } + master.variables = scope.variables; + } + if (form.fields[fld].type == 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } + } } + scope.url = data.url; var related = data.related; for (var set in form.related) { if (related[set]) { - relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; + relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; } } @@ -586,9 +601,8 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route scope.$emit('jobTemplateLoaded', data.related.cloud_credential); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to retrieve job template: ' + $routeParams.id + '. GET status: ' + status }); + { hdr: 'Error!', msg: 'Failed to retrieve job template: ' + $routeParams.id + '. GET status: ' + status }); }); // Save changes to the parent @@ -634,7 +648,6 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route (base == 'job_templates') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update job template. PUT returned status: ' + status }); }); diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 1c806bb825..c31b1b635a 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -198,6 +198,7 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, var master = {}; var id = $routeParams.id; var relatedSets = {}; + var loadingFinishedCount = 0; scope.job_id = id; scope.parseType = 'yaml'; @@ -213,44 +214,28 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, for (var i=0; i < data.length; i++) { scope.playbook_options.push(data[i]); } + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { - //ProcessErrors(scope, data, status, form, - // { hdr: 'Error!', msg: 'Failed to get playbook list for ' + url +'. GET returned status: ' + status }); - - // Ignore the error. We get this error when the project or playbook has been deleted - + scope.$emit('jobTemplateLoadFinished'); }); } + else { + scope.$emit('jobTemplateLoadFinished'); + } } - // Register a watcher on project_name. Refresh the playbook list on change. - if (scope.selectPlaybookUnregister) { - scope.selectPlaybookUnregister(); - } - scope.selectPlaybookUnregister = scope.$watch('project_name', function(oldValue, newValue) { - if (oldValue !== newValue && newValue !== '' && newValue !== null && newValue !== undefined) { - scope.playbook = null; - getPlaybooks(scope.project); - } - }); - // Retrieve each related set and populate the playbook list if (scope.jobLoadedRemove) { scope.jobLoadedRemove(); } scope.jobLoadedRemove = scope.$on('jobLoaded', function(e, related_cloud_credential) { - scope[form.name + 'ReadOnly'] = (scope.status == 'new') ? false : true; - - // Load related sets - for (var set in relatedSets) { - scope.search(relatedSets[set].iterator); - } - // Set the playbook lookup getPlaybooks(scope.project); + scope[form.name + 'ReadOnly'] = (scope.status == 'new') ? false : true; + $('#forks-slider').slider("option", "value", scope.forks); $('#forks-slider').slider("disable"); $('input[type="checkbox"]').attr('disabled','disabled'); @@ -271,6 +256,7 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, default_val: dft }); scope['callback_url'] = data.related['callback']; + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { scope['callback_url'] = '<< Job template not found >>'; @@ -282,15 +268,29 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, Rest.get() .success( function(data, status, headers, config) { scope['cloud_credential_name'] = data.name; + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to related cloud credential. GET returned status: ' + status }); }); } - - Wait('stop'); + else { + scope.$emit('jobTemplateLoadFinished'); + } + }); + // Turn off 'Wait' after both cloud credential and playbook list come back + if (scope.removeJobTemplateLoadFinished) { + scope.removeJobTemplateLoadFinished(); + } + scope.removeJobTemplateLoadFinished = scope.$on('jobTemplateLoadFinished', function() { + loadingFinishedCount++; + if (loadingFinishedCount >= 3) { + // The initial template load finished. Now load related jobs, which + // will turn off the 'working' spinner. + Wait('stop'); + } }); // Our job type options diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index 261a167f64..bc85bf9eb5 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -200,7 +200,7 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, }); // Retrieve detail record and prepopulate the form - Wait('stop'); + Wait('start'); Rest.setUrl(defaultUrl + ':id/'); Rest.get({ params: {id: id} }) .success( function(data, status, headers, config) { diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index 274a4a0d13..bfb87cf176 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -325,7 +325,7 @@ angular.module('JobTemplateFormDefinition', []) { name: "error", value: "error" }, { name: "failed", value: "failed" }, { name: "canceled", value: "canceled" } ], - badgeIcon: 'icon-job-\{\{ job.status \}\}', + badgeIcon: 'fa icon-job-\{\{ job.status \}\}', badgePlacement: 'left', badgeToolTip: "\{\{ job.statusBadgeToolTip \}\}", badgeTipPlacement: 'top', diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js index bcab66394c..d194dd74ca 100644 --- a/awx/ui/static/js/helpers/PaginationHelpers.js +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -52,8 +52,8 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat } }]) - .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', - function(RefreshRelated, $cookieStore) { + .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', 'Wait', + function(RefreshRelated, $cookieStore, Wait) { return function(params) { var scope = params.scope; @@ -80,7 +80,7 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; Wait('start'); - RefreshRefresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + RefreshRelated({ scope: scope, set: set, iterator: iterator, url: new_url }); } scope.pageIsActive = function(page, iterator) { From 7434bb8a9ed94131fcb67472ecd9254aba7dfd4c Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 15:57:00 -0500 Subject: [PATCH 07/22] AC-985 edit now edit's inventory properties via dialog- using the same edit code already written for the wrench button on inventory->groups/hosts page. --- awx/ui/static/js/controllers/Inventories.js | 16 ++++++- awx/ui/static/js/helpers/Groups.js | 1 - awx/ui/static/js/helpers/inventory.js | 48 +++++++++++++++++---- awx/ui/static/js/helpers/search.js | 32 ++++++++++---- awx/ui/static/js/lists/Inventories.js | 2 +- awx/ui/static/js/widgets/Stream.js | 3 +- 6 files changed, 80 insertions(+), 22 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 6547d9565c..773f340263 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -12,7 +12,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, InventoryList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, Wait, Stream) + ClearScope, ProcessErrors, GetBasePath, Wait, Stream, EditInventoryProperties) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -128,9 +128,21 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res } }); + + if (scope.removeRefreshInventories) { + scope.removeRefreshInventories(); + } + scope.removeRefreshInventories = scope.$on('RefreshInventories', function() { + // Reflect changes after inventory properties edit completes + scope.search(list.iterator); + }); scope.showActivity = function() { Stream({ scope: scope }); } + scope.editInventoryProperties = function(inventory_id) { + EditInventoryProperties({ scope: scope, inventory_id: inventory_id }); + } + scope.addInventory = function() { $location.path($location.path() + '/add'); } @@ -186,7 +198,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res InventoriesList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'InventoryList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'Wait', 'Stream' ]; + 'GetBasePath', 'Wait', 'Stream', 'EditInventoryProperties']; function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, InventoryForm, diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 4624e1eb89..bc43f150ce 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -27,7 +27,6 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.options() .success( function(data, status, headers, config) { var choices = data.actions.GET.source.choices - console.log(choices); for (var i=0; i < choices.length; i++) { if (choices[i][0] !== 'file') { scope[variable].push({ diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index 30258b9c05..ce5be78385 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -9,7 +9,7 @@ */ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', 'ListGenerator', 'AuthService', - 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper' + 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'SearchHelper' ]) .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', @@ -20,6 +20,7 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi // Save inventory property modifications var scope = params.scope; + var form = InventoryForm; var defaultUrl=GetBasePath('inventory'); @@ -71,7 +72,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update inventory. POST returned status: ' + status }); }); @@ -85,9 +85,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi .factory('EditInventoryProperties', ['InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'SaveInventory', 'Wait', + 'GetBasePath', 'ParseTypeChange', 'SaveInventory', 'Wait', 'Store', 'SearchInit', function(InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, SaveInventory, - Wait) { + Wait, Store, SearchInit) { return function(params) { var parent_scope = params.scope @@ -98,6 +98,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var defaultUrl=GetBasePath('inventory'); var master = {}; + // Hang onto current search params + var PreviousSearchParams = Store('CurrentSearchParams'); + form.well = false; //form.formLabelSize = 'col-lg-3'; //form.formFieldSize = 'col-lg-9'; @@ -116,8 +119,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi scope.formModalInfo = false; scope.formModalHeader = 'Inventory Properties'; - $('#form-modal .btn-success').removeClass('btn-none').addClass('btn-success'); - Wait('start'); Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); Rest.get() @@ -170,7 +171,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status }); }); @@ -179,11 +179,41 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi scope.removeInventorySaved(); } scope.removeInventorySaved = scope.$on('InventorySaved', function() { - $('#form-modal').modal('hide'); + $('#form-modal').modal('hide'); + // Restore prior search state + if (scope.searchCleanp) { + scope.searchCleanup(); + } + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + parent_scope.$emit('RefreshInventories'); }); + scope.cancelModal = function() { + // Restore prior search state + if (scope.searchCleanp) { + scope.searchCleanup(); + } + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + } + scope.formModalAction = function() { - scope.inventory_id = inventory_id; + parent_scope.inventory_id = inventory_id; parent_scope.inventory_name = scope.inventory_name; SaveInventory({ scope: scope }); } diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 422636cf2f..3d4116781d 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -27,14 +27,15 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var list = params.list; var iterator = (params.iterator) ? params.iterator : list.iterator; var setWidgets = (params.setWidgets == false) ? false : true; - - var sort_order, expected_objects=0, found_objects=0; + var sort_order = params.sort_order || ''; + var expected_objects=0, found_objects=0; var params = { set: set, defaultUrl: defaultUrl, list: list, - iterator: iterator + iterator: iterator, + sort_order: sort_order }; Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore @@ -136,6 +137,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } + var params = { + set: set, + defaultUrl: defaultUrl, + list: list, + iterator: iterator, + sort_order: sort_order + }; + + Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore + + // Functions to handle search widget changes scope.setSearchField = function(iterator, fld, label, widget) { @@ -237,7 +249,8 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) // scope[iterator + 'Loading'] = (load == undefined || load == true) ? true : false; var url = defaultUrl; - + var connect; + //finalize and execute the query scope[iterator + 'Page'] = (page) ? parseInt(page) - 1 : 0; if (scope[iterator + 'SearchParams']) { @@ -248,14 +261,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) url += '&' + scope[iterator + 'SearchParams']; } } - url = url.replace(/\&\&/,'&'); - url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : ""; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += (scope[iterator + '_page_size']) ? connect + 'page_size=' + scope[iterator + '_page_size'] : ""; if (page) { - url += '&page=' + page; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += connect + 'page=' + page; } if (scope[iterator + 'ExtraParms']) { - url += scope[iterator + 'ExtraParms']; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += connect + scope[iterator + 'ExtraParms']; } + url = url.replace(/\&\&/,'&'); Refresh({ scope: scope, set: set, iterator: iterator, url: url }); }); diff --git a/awx/ui/static/js/lists/Inventories.js b/awx/ui/static/js/lists/Inventories.js index d33a32ccf5..d089c7194e 100644 --- a/awx/ui/static/js/lists/Inventories.js +++ b/awx/ui/static/js/lists/Inventories.js @@ -84,7 +84,7 @@ angular.module('InventoriesListDefinition', []) }, edit: { label: 'Edit', - ngClick: "editInventory(\{\{ inventory.id \}\})", + ngClick: 'editInventoryProperties(inventory.id)', awToolTip: 'Edit inventory', dataPlacement: 'top' }, diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index 5fa61f73f2..d13d76a525 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -267,7 +267,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti var parent_scope = params.scope; // Hang onto current search params - var PreviousSearchParams = Store('SearchInitParams'); + var PreviousSearchParams = Store('CurrentSearchParams'); // pass in an inventory name to fix breadcrumb display var inventory_name = (params) ? params.inventory_name : null; @@ -340,6 +340,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti list: PreviousSearchParams.list, url: PreviousSearchParams.defaultUrl, iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, setWidgets: false }); } if (inUrl) { From 0e6305c9ec4024eb51575662cb32ca717564c83f Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 17:22:34 -0500 Subject: [PATCH 08/22] AC-987 job launch shows working 2x --- awx/ui/static/js/controllers/Inventories.js | 2 +- awx/ui/static/js/helpers/JobSubmission.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 773f340263..7960a6257d 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -94,7 +94,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res else { // many hosts with 0 failures scope.inventories[i].failed_hosts_tip = scope.inventories[i].total_hosts + - ( (scope.inventories[i].total_hosts > 1) ? ' hosts' : ' host' ) + " with no failures. Click to view details."; + ( (scope.inventories[i].total_hosts > 1) ? ' hosts' : ' host' ) + " with no job failures. Click to view details."; scope.inventories[i].failed_hosts_link = '/#/inventories/' + scope.inventories[i].id + '/'; scope.inventories[i].failed_hosts_class = 'false'; } diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 5a6e040f70..de4495d28d 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -230,7 +230,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential extra_vars: data.extra_vars }) .success( function(data, status, headers, config) { - Wait('stop'); scope.job_id = data.id; if (data.passwords_needed_to_start.length > 0) { // Passwords needed. Prompt for passwords, then start job. @@ -243,7 +242,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } else { // No passwords needed, start the job! - Wait('start'); Rest.setUrl(data.related.start); Rest.post() .success( function(data, status, headers, config) { @@ -257,7 +255,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to start job. POST returned status: ' + status }); }); @@ -275,11 +272,11 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); // Create a job record scope.credential = ''; if (data.credential == '' || data.credential == null) { // Template does not have credential, prompt for one + Wait('stop'); if (scope.credentialWatchRemove) { scope.credentialWatchRemove(); } @@ -309,7 +306,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get job template details. GET returned status: ' + status }); }); @@ -394,7 +390,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get project update details: ' + url + ' GET status: ' + status }); }); From d57b2f79265016419fa2089d84d6dbfbc9ad1412 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 17:46:47 -0500 Subject: [PATCH 09/22] AC-979 using activity stream object arrays correctly. AC-980 fixed Action label on detail dialog. --- awx/ui/static/js/forms/ActivityDetail.js | 14 +----- awx/ui/static/js/widgets/Stream.js | 60 ++++++++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/awx/ui/static/js/forms/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js index 80e0b7df34..557e586219 100644 --- a/awx/ui/static/js/forms/ActivityDetail.js +++ b/awx/ui/static/js/forms/ActivityDetail.js @@ -23,22 +23,10 @@ angular.module('ActivityDetailDefinition', []) readonly: true }, operation: { - label: 'Operation', + label: 'Action', type: 'text', readonly: true }, - /*object1_name: { - label: '\{\{ object1 \}\}', - type: 'text', - ngHide: '!object1', - readonly: true - }, - object2_name: { - label: '\{\{ object2 \}\}', - type: 'text', - ngHide: '!object2', - readonly: true - },*/ changes: { label: 'Changes', type: 'textarea', diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index d13d76a525..e0a628db41 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -146,41 +146,51 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti descr += activity.operation; descr += (/e$/.test(activity.operation)) ? 'd ' : 'ed '; descr_nolink = descr; + + // labels var obj1 = activity.object1; var obj2 = activity.object2; + // objects + var obj1_obj = (activity.summary_fields[obj1]) ? activity.summary_fields[obj1][0] : null; + if (obj1 == obj2) { + var obj2_obj = activity.summary_fields[obj1][1]; + } + else if (activity.summary_fields[obj2]) { + var obj2_obj = activity.summary_fields[obj2][0]; + } + else { + var obj2_obj = null; + } + if (obj1 == 'user' || obj2 == 'user') { activity.summary_fields['user'][0].name = activity.summary_fields['user'][0].username; } var name; - if (activity.summary_fields[obj2] && activity.summary_fields[obj2][0].name - && !/^_delete/.test(activity.summary_fields[obj2][0].name)) { - activity.summary_fields[obj2][0]['base'] = obj2; - descr += obj2 + ' ' - + activity.summary_fields[obj2][0].name + '' + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); - descr_nolink += obj2 + ' ' + activity.summary_fields[obj2][0].name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) { + obj2_obj['base'] = obj2; + descr += obj2 + ' ' + + obj2_obj.name + '' + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr_nolink += obj2 + ' ' + obj2_obj.name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - else if (activity.object2) { + else if (obj2) { name = ''; - if (activity.summary_fields[obj2] && activity.summary_fields[obj2][0].name) { - name = ' ' + stripDeleted(activity.summary_fields[obj2][0].name); + if (obj2_obj && obj2_obj.name) { + name = ' ' + stripDeleted(obj2_obj.name); } - descr += activity.object2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); - descr_nolink += activity.object2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr_nolink += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - if (activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name - && !/^\_delete/.test(activity.summary_fields[obj1][0].name)) { - activity.summary_fields[obj1][0]['base'] = obj1; - descr += obj1 + ' ' - + activity.summary_fields[obj1][0].name + ''; - descr_nolink += obj1 + ' ' + activity.summary_fields[obj1][0].name; + if (obj2_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { + obj1_obj['base'] = obj1; + descr += obj1 + ' ' + obj1_obj.name + ''; + descr_nolink += obj1 + ' ' + obj1_obj.name; } - else if (activity.object1) { + else if (obj1) { name = ''; - if ( ((!(activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name)) || - activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name && - /^_delete/.test(activity.summary_fields[obj1][0].name)) + // find the name in changes, if needed + if ( ((!(obj1_obj && obj1_obj.name)) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name)) && (activity.changes && activity.changes.name) ) { if (typeof activity.changes.name == 'string') { name = ' ' + activity.changes.name; @@ -189,11 +199,11 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti name = ' ' + activity.changes.name[0] } } - else if (activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name) { - name = ' ' + stripDeleted(activity.summary_fields[obj1][0].name); + else if (obj1_obj && obj1_obj.name) { + name = ' ' + stripDeleted(obj1_obj.name); } - descr += activity.object1 + name; - descr_nolink += activity.object1 + name; + descr += obj1 + name; + descr_nolink += obj1 + name; } activity['description'] = descr; activity['description_nolink'] = descr_nolink; From 7580b91186c3f4846f87f1d6f46b1f631e0b70ae Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 18:47:46 -0500 Subject: [PATCH 10/22] AC-976 add job template name to activity stream --- awx/ui/static/js/helpers/search.js | 13 ++++++++--- awx/ui/static/js/lists/Streams.js | 18 +++++++++++++++ awx/ui/static/js/widgets/Stream.js | 35 ++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 3d4116781d..b1ab7e8373 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -301,10 +301,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) if (scope[iterator + 'SearchValue' + modifier]) { // A search value was entered scope[iterator + 'ShowStartBtn' + modifier] = false; - scope[iterator + 'SearchParams'] += '&' + + if (list.fields[scope[iterator + 'SearchField' + modifier]].searchOnID) { + scope[iterator + 'SearchParams'] += '&' + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + - '__name__icontains=' + - scope[iterator + 'SearchValue' + modifier]; + '__id=' + scope[iterator + 'SearchValue' + modifier]; + } + else { + scope[iterator + 'SearchParams'] += '&' + + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + + '__name__icontains=' + + scope[iterator + 'SearchValue' + modifier]; + } } else { // Search value is empty diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js index d4f47358b0..1c80032d2b 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -102,6 +102,15 @@ angular.module('StreamListDefinition', []) searchWidget: 2, searchField: 'object1' }, + job_search: { + label: 'Job', + searchOnly: true, + searchObject: 'job', + searchPlaceholder: 'Job id', + searchOnID: true, + searchWidget: 2, + searchField: 'object1' + }, organization_search: { label: 'Organization', searchOnly: true, @@ -169,6 +178,15 @@ angular.module('StreamListDefinition', []) searchWidget: 3, searchField: 'object2' }, + job_search3: { + label: 'Job', + searchOnly: true, + searchObject: 'job', + searchPlaceholder: 'Job id', + searchOnID: true, + searchWidget: 3, + searchField: 'object2' + }, job_template_search3: { label: 'Job Template', searchOnly: true, diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index e0a628db41..7bef535ed8 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -182,7 +182,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti descr += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); descr_nolink += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - if (obj2_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { + if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { obj1_obj['base'] = obj1; descr += obj1 + ' ' + obj1_obj.name + ''; descr_nolink += obj1 + ' ' + obj1_obj.name; @@ -190,13 +190,34 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti else if (obj1) { name = ''; // find the name in changes, if needed - if ( ((!(obj1_obj && obj1_obj.name)) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name)) - && (activity.changes && activity.changes.name) ) { - if (typeof activity.changes.name == 'string') { - name = ' ' + activity.changes.name; + if ( !(obj1_obj && obj1_obj.name) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name) ) { + if (activity.changes && activity.changes.name) { + if (typeof activity.changes.name == 'string') { + name = ' ' + activity.changes.name; + } + else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { + name = ' ' + activity.changes.name[0] + } } - else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { - name = ' ' + activity.changes.name[0] + else if (obj1 == 'job' && obj1_obj && activity.changes && activity.changes.job_template) { + // Hack for job activity where the template name is known + if (activity.operation != 'delete') { + obj1_obj['base'] = obj1; + name = ' ' + ''+ obj1_obj.id + ' ' + activity.changes.job_template + ''; + } + else { + name = ' ' + obj1_obj.id + ' ' + activity.changes.job_template; + } + } + else if (obj1 == 'job' && obj1_obj) { + // Hack for job activity where template name not known + if (activity.operation != 'delete') { + obj1_obj['base'] = obj1; + name = ' ' + '' + obj1_obj.id + ''; + } + else { + name = ' ' + obj1_obj.id; + } } } else if (obj1_obj && obj1_obj.name) { From 9e119fc8a935b73939e0fd1f94b4455f1f5a1d25 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 29 Jan 2014 17:30:12 +0000 Subject: [PATCH 11/22] Rename package awx to ansible-tower --- Makefile | 30 +++++++++++++++--------------- docs/build_system.md | 2 +- setup.py | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 6b127cfb41..c1d2ab09a1 100644 --- a/Makefile +++ b/Makefile @@ -15,20 +15,20 @@ PACKER_LICENSE_FILE ?= test.json ifneq ($(OFFICIAL),yes) BUILD=dev$(DATE) -SDIST_TAR_FILE=awx-$(VERSION)-$(BUILD).tar.gz -SETUP_TAR_NAME=awx-setup-$(VERSION)-$(BUILD) +SDIST_TAR_FILE=ansible-tower-$(VERSION)-$(BUILD).tar.gz +SETUP_TAR_NAME=ansible-tower-setup-$(VERSION)-$(BUILD) RPM_PKG_RELEASE=$(BUILD) -DEB_BUILD_DIR=deb-build/awx-$(VERSION)-$(BUILD) +DEB_BUILD_DIR=deb-build/ansible-tower-$(VERSION)-$(BUILD) DEB_PKG_RELEASE=$(VERSION)-$(BUILD) -PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-nightly.json +PACKER_BUILD_OPTS=-var-file=vars-aws-keys.json -var-file=vars-nightly.json else BUILD= -SDIST_TAR_FILE=awx-$(VERSION).tar.gz -SETUP_TAR_NAME=awx-setup-$(VERSION) +SDIST_TAR_FILE=ansible-tower-$(VERSION).tar.gz +SETUP_TAR_NAME=ansible-tower-setup-$(VERSION) RPM_PKG_RELEASE=$(RELEASE) -DEB_BUILD_DIR=deb-build/awx-$(VERSION) +DEB_BUILD_DIR=deb-build/ansible-tower-$(VERSION) DEB_PKG_RELEASE=$(VERSION)-$(RELEASE) -PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-release.json +PACKER_BUILD_OPTS=-var-file=vars-aws-keys.json -var-file=vars-release.json endif .PHONY: clean rebase push requirements requirements_pypi develop refresh \ @@ -168,13 +168,13 @@ sdist: clean minjs rpmtar: sdist if [ "$(OFFICIAL)" != "yes" ] ; then \ (cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \ - (cd dist/ && mv awx-$(VERSION)-$(BUILD) awx-$(VERSION)) ; \ - (cd dist/ && tar czf awx-$(VERSION).tar.gz awx-$(VERSION)) ; \ + (cd dist/ && mv ansible-tower-$(VERSION)-$(BUILD) ansible-tower-$(VERSION)) ; \ + (cd dist/ && tar czf ansible-tower-$(VERSION).tar.gz ansible-tower-$(VERSION)) ; \ fi rpm: rpmtar @mkdir -p rpm-build - @cp dist/awx-$(VERSION).tar.gz rpm-build/ + @cp dist/ansible-tower-$(VERSION).tar.gz rpm-build/ @rpmbuild --define "_topdir %(pwd)/rpm-build" \ --define "_builddir %{_topdir}" \ --define "_rpmdir %{_topdir}" \ @@ -183,23 +183,23 @@ rpm: rpmtar --define '_rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm' \ --define "_sourcedir %{_topdir}" \ --define "_pkgrelease $(RPM_PKG_RELEASE)" \ - -ba packaging/rpm/awx.spec + -ba packaging/rpm/ansible-tower.spec deb: sdist @mkdir -p deb-build @cp dist/$(SDIST_TAR_FILE) deb-build/ (cd deb-build && tar zxf $(SDIST_TAR_FILE)) - (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p awx-$(VERSION)) + (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p ansible-tower-$(VERSION)) @rm -rf $(DEB_BUILD_DIR)/debian @cp -a packaging/debian $(DEB_BUILD_DIR)/ - @echo "awx_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles + @echo "ansible-tower_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE_FILE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" ami: packer_license - (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) + (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" ansible-tower.json) install: $(PYTHON) setup.py install egg_info -b "" diff --git a/docs/build_system.md b/docs/build_system.md index 4c961b3926..d1a80d30f9 100644 --- a/docs/build_system.md +++ b/docs/build_system.md @@ -267,7 +267,7 @@ These nightly repositories can be used by the AWX setup playbook by running the As noted above, `OFFICIAL` builds are copied out to the production server, and can be found at the following location: - http://releases.ansible.com/awx/ + http://releases.ansible.com/ansible-tower/ The AWX setup playbook will use this repo location by default. diff --git a/setup.py b/setup.py index 3d089aa5e6..7e3284d4e2 100755 --- a/setup.py +++ b/setup.py @@ -105,11 +105,11 @@ class sdist_awx(_sdist, object): super(sdist_awx, self).make_distribution() setup( - name='awx', + name='ansible-tower', version=__version__.split("-")[0], # FIXME: Should keep full version here? - author='AnsibleWorks, Inc.', - author_email='support@ansibleworks.com', - description='AWX: API, UI and Task Engine for Ansible', + author='Ansible, Inc.', + author_email='support@ansible.com', + description='ansible-tower: API, UI and Task Engine for Ansible', long_description='AWX provides a web-based user interface, REST API and ' 'task engine built on top of Ansible', license='Proprietary', From 97b776cf2381f862f0f80a2b3b5c9a43f39bae09 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 29 Jan 2014 13:10:21 -0500 Subject: [PATCH 12/22] Fix ansible-tower deb build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c1d2ab09a1..02d71e5e5a 100644 --- a/Makefile +++ b/Makefile @@ -192,7 +192,7 @@ deb: sdist (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p ansible-tower-$(VERSION)) @rm -rf $(DEB_BUILD_DIR)/debian @cp -a packaging/debian $(DEB_BUILD_DIR)/ - @echo "ansible-tower_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles + @echo "ansible-tower-$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: From 22f40605cd4bf996da893a015bb6ee10e832599a Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 13:31:36 -0500 Subject: [PATCH 13/22] AC-976 now using custom javascript to apply ellipsis to long group and host names. Fixed indes.html title. --- awx/ui/static/js/controllers/Inventories.js | 13 ++- awx/ui/static/js/helpers/Groups.js | 26 ++++-- awx/ui/static/js/helpers/Hosts.js | 63 +++++++++++---- awx/ui/static/js/helpers/inventory.js | 20 +++-- awx/ui/static/js/lists/InventoryGroups.js | 1 - awx/ui/static/js/lists/InventoryHosts.js | 5 +- awx/ui/static/less/ansible-ui.less | 30 +++++-- awx/ui/static/lib/ansible/InventoryTree.js | 18 ++++- awx/ui/static/lib/ansible/Utilities.js | 79 +++++++++++++++---- awx/ui/static/lib/ansible/directives.js | 1 - .../static/lib/ansible/generator-helpers.js | 20 ++++- awx/ui/templates/ui/index.html | 4 +- 12 files changed, 216 insertions(+), 64 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 7960a6257d..44208ce494 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -316,7 +316,7 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis GetSyncStatusMsg, InjectHosts, HostsReload, GroupsAdd, GroupsEdit, GroupsDelete, Breadcrumbs, LoadBreadCrumbs, Empty, Rest, ProcessErrors, InventoryUpdate, Alert, ToggleChildren, ViewUpdateStatus, GroupsCancelUpdate, Find, HostsCreate, EditInventoryProperties, HostsEdit, HostsDelete, ToggleHostEnabled, CopyMoveGroup, CopyMoveHost, - Stream, GetBasePath, ShowJobSummary) + Stream, GetBasePath, ShowJobSummary, ApplyEllipsis, WatchInventoryWindowResize) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -360,6 +360,11 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis // Add hosts view $scope.show_failures = false; InjectHosts({ scope: $scope, inventory_id: $scope.inventory_id, tree_id: $scope.selected_tree_id, group_id: $scope.selected_group_id }); + + // As the window shrinks and expands, apply ellipsis + setTimeout(function() { ApplyEllipsis('#groups_table .group-name a'); }, 2500); //give the window time to display + WatchInventoryWindowResize(); + }); @@ -368,6 +373,8 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis $scope.removeGroupTreeRefreshed(); } $scope.removeGroupTreeRefreshed = $scope.$on('GroupTreeRefreshed', function(e, inventory_name, groups) { + // Reapply ellipsis to groups + setTimeout(function() { ApplyEllipsis('#groups_table .group-name a'); }, 2500); // Reselect the preveiously selected group node, causing host view to refresh. $scope.showHosts($scope.selected_tree_id, $scope.selected_group_id, false); }); @@ -525,12 +532,14 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis //Load tree data for the first time BuildTree({ scope: $scope, inventory_id: $scope.inventory_id, refresh: false }); + } InventoriesEdit.$inject = [ '$scope', '$location', '$routeParams', '$compile', 'GenerateList', 'ClearScope', 'InventoryGroups', 'InventoryHosts', 'BuildTree', 'Wait', 'GetSyncStatusMsg', 'InjectHosts', 'HostsReload', 'GroupsAdd', 'GroupsEdit', 'GroupsDelete', 'Breadcrumbs', 'LoadBreadCrumbs', 'Empty', 'Rest', 'ProcessErrors', 'InventoryUpdate', 'Alert', 'ToggleChildren', 'ViewUpdateStatus', 'GroupsCancelUpdate', 'Find', 'HostsCreate', 'EditInventoryProperties', 'HostsEdit', - 'HostsDelete', 'ToggleHostEnabled', 'CopyMoveGroup', 'CopyMoveHost', 'Stream', 'GetBasePath', 'ShowJobSummary' + 'HostsDelete', 'ToggleHostEnabled', 'CopyMoveGroup', 'CopyMoveHost', 'Stream', 'GetBasePath', 'ShowJobSummary', + 'ApplyEllipsis', 'WatchInventoryWindowResize' ]; diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index bc43f150ce..073620cd2d 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -341,10 +341,10 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('GroupsAdd', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'ParseTypeChange', 'GroupsEdit', 'Wait', 'GetChoices', - 'GetSourceTypeOptions', 'LookUpInit', 'BuildTree', 'SourceChange', + 'GetSourceTypeOptions', 'LookUpInit', 'BuildTree', 'SourceChange', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, ParseTypeChange, GroupsEdit, Wait, GetChoices, GetSourceTypeOptions, LookUpInit, BuildTree, - SourceChange) { + SourceChange, WatchInventoryWindowResize) { return function(params) { var inventory_id = params.inventory_id; @@ -387,11 +387,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } scope.removeSaveComplete = scope.$on('SaveComplete', function(e, group_id, error) { if (!error) { - if (scope.searchCleanup) + if (scope.searchCleanup) { scope.searchCleanup(); + } scope.formModalActionDisabled = false; scope.showGroupHelp = false; //get rid of the Hint BuildTree({ scope: parent_scope, inventory_id: inventory_id, refresh: true, new_group_id: group_id }); + WatchInventoryWindowResize(); } }); @@ -484,6 +486,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' if (scope.searchCleanup) { scope.searchCleanup(); } + WatchInventoryWindowResize(); } // Save @@ -589,9 +592,10 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('GroupsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate', 'GetUpdateIntervalOptions', 'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find', + 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, GetUpdateIntervalOptions, - LookUpInit, Empty, Wait, GetChoices, UpdateGroup, SourceChange, Find) { + LookUpInit, Empty, Wait, GetChoices, UpdateGroup, SourceChange, Find, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -855,6 +859,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' else { Wait('stop'); } + WatchInventoryWindowResize(); } }); @@ -936,6 +941,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' if (scope.searchCleanup) { scope.searchCleanup(); } + WatchInventoryWindowResize(); } // Save @@ -1076,9 +1082,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('ShowUpdateStatus', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventoryStatusForm', 'Wait', 'Empty', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventoryStatusForm', 'Wait', 'Empty', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath, - FormatDate, InventoryStatusForm, Wait, Empty) { + FormatDate, InventoryStatusForm, Wait, Empty, WatchInventoryWindowResize) { return function(params) { var group_name = params.group_name; @@ -1102,9 +1108,15 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' scope.formModalAction = function() { $('#form-modal').modal("hide"); if (parent_scope && parent_scope.showHosts && !Empty(tree_id)) { - if (parent_scope.selected_tree_id !== tree_id) + if (parent_scope.selected_tree_id !== tree_id) { parent_scope.showHosts(tree_id, group_id, false); + } } + WatchInventoryWindowResize(); + } + + scope.cancelModal = function() { + WatchInventoryWindowResize(); } if (scope.removeUpdateStatusReady) { diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index 74df35b788..beb4d89a8f 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -77,21 +77,21 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H return -1 * (a - b); }); title = "Recent Jobs"; - html = "\n"; + html = "
\n"; html += "\n"; html += "\n"; html += "\n"; html += "\n"; html += "\n"; for (var j=0; j < jobs.length; j++) { var job = jobs[j]; html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; html += "\n"; } html += "\n"; @@ -128,8 +128,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H }]) .factory('HostsReload', [ '$routeParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'SearchInit', 'PaginateInit', 'Wait', - 'SetHostStatus', 'SetStatus', - function($routeParams, Empty, InventoryHosts, GetBasePath, SearchInit, PaginateInit, Wait, SetHostStatus, SetStatus) { + 'SetHostStatus', 'SetStatus', 'ApplyEllipsis', + function($routeParams, Empty, InventoryHosts, GetBasePath, SearchInit, PaginateInit, Wait, SetHostStatus, SetStatus, + ApplyEllipsis) { return function(params) { var scope = params.scope; @@ -153,6 +154,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H //SetHostStatus(scope.hosts[i]); } SetStatus({ scope: scope }); + setTimeout(function() { ApplyEllipsis('#hosts_table .host-name a'); }, 2500); Wait('stop'); scope.$emit('HostReloadComplete'); }); @@ -304,9 +306,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H .factory('HostsCreate', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange, Wait) { + GetBasePath, HostsReload, ParseTypeChange, Wait, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -349,11 +351,14 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H scope.removeHostSaveComplete = scope.$on('HostSaveComplete', function() { Wait('stop'); $('#form-modal').modal('hide'); + HostsReload({ scope: parent_scope, group_id: parent_scope.selected_group_id, tree_id: parent_scope.selected_tree_id, inventory_id: parent_scope.inventory_id }); + + WatchInventoryWindowResize(); }); // Save @@ -417,15 +422,20 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H // Defaults generator.reset(); }; - + + scope.cancelModal = function() { + WatchInventoryWindowResize(); + } + } }]) .factory('HostsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', + 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus) { + GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -513,13 +523,29 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H scope.removeSaveCompleted = scope.$on('saveCompleted', function() { // Update the name on the list var host = Find({ list: parent_scope.hosts, key: 'id', val: host_id }); + var old_name = host.name; host.name = scope.name; host.enabled = scope.enabled; host.enabled_flag = scope.enabled; SetStatus({ scope: parent_scope, host: host }); - // Close modal - Wait('stop'); - $('#form-modal').modal('hide'); + + // Update any titles attributes created by ApplyEllipsis + if (old_name) { + setTimeout(function() { + $('#hosts_table .host-name a[title="' + old_name + '"').attr('title', host.name); + ApplyEllipsis('#hosts_table .host-name a'); + // Close modal + Wait('stop'); + $('#form-modal').modal('hide'); + }, 2000); + } + else { + // Close modal + Wait('stop'); + $('#form-modal').modal('hide'); + } + // Restore ellipsis response to window resize + WatchInventoryWindowResize(); }); // Save changes to the parent @@ -580,6 +606,11 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H } scope.parseType = 'yaml'; } + + scope.cancelModal = function() { + WatchInventoryWindowResize(); + } + } }]) diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index ce5be78385..d88f5132e5 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -12,6 +12,20 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'SearchHelper' ]) + .factory('WatchInventoryWindowResize', ['ApplyEllipsis', function(ApplyEllipsis) { + return function() { + // Call to set or restore window resize + var timeOut; + $(window).resize(function() { + clearTimeout(timeOut); + timeOut = setTimeout(function() { + ApplyEllipsis('#groups_table .group-name a'); + ApplyEllipsis('#hosts_table .host-name a'); + }, 100); + }); + } + }]) + .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', 'GetBasePath', 'ParseTypeChange', 'Wait', function(InventoryForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, Wait) { @@ -102,16 +116,12 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var PreviousSearchParams = Store('CurrentSearchParams'); form.well = false; - //form.formLabelSize = 'col-lg-3'; - //form.formFieldSize = 'col-lg-9'; var scope = generator.inject(form, {mode: 'edit', modal: true, related: false, modal_show: false }); /* Reset form properties. Otherwise it screws up future requests of the Inventories detail page */ form.well = true; - //delete form.formLabelSize; - //delete form.formFieldSize; - + ParseTypeChange(scope,'inventory_variables', 'inventoryParseType'); scope.inventoryParseType = 'yaml'; scope.formModalActionLabel = 'Save'; diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index f341e42b69..79b8e9750f 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -29,7 +29,6 @@ angular.module('InventoryGroupsDefinition', []) ngClass: "group.selected_class", hasChildren: true, columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', - 'class': 'ellipsis', nosort: true, awDroppable: "\{\{ group.isDroppable \}\}", awDraggable: "\{\{ group.isDraggable \}\}", diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index d59854352b..3efae93e01 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -26,7 +26,7 @@ angular.module('InventoryHostsDefinition', []) label: 'Hosts', ngClick: "editHost(\{\{ host.id \}\})", searchPlaceholder: "search_place_holder", - columnClass: 'col-lg-9 ellipsis', + columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', dataHostId: "\{\{ host.id \}\}", dataType: "host", awDraggable: "true" @@ -91,6 +91,9 @@ angular.module('InventoryHostsDefinition', []) }, actions: { + + columnClass: 'col-lg-3 col-md-3 col-sm-5 col-xs-5', + create: { mode: 'all', ngClick: "createHost()", diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 0666752e3d..306d5288cb 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -66,7 +66,12 @@ body.modal-open { .ellipsis { white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; + text-overflow: ellipsis; +} + +.group-name { + display: inline-block; + width: 90%; } a { @@ -193,20 +198,22 @@ textarea { } /* TB tooltip overrides */ - + .popover-content, .popover-content { + width: 100%; + } .popover { - z-index: 2000; + z-index: 2000; } .tooltip { - z-index: 1050; - opacity: 1.0; + z-index: 1050; + opacity: 1.0; } - .alert { +.alert { margin-top: 15px; margin-bottom: 15px; - } +} hr { border-color: #e3e3e3; @@ -1132,6 +1139,10 @@ input[type="checkbox"].checkbox-no-label { .draggable-clone { opacity: .60; font-weight: bold; + /*z-index: 2000; + overflow: visible; + whitespace: wrap; + text-overflow: clip;*/ } .droppable-hover { @@ -1139,7 +1150,10 @@ input[type="checkbox"].checkbox-no-label { color: @info-color; padding: 6px; border: 1px solid @info-border; - border-radius: 4px; + border-radius: 4px; + /*overflow: visible; + whitespace: wrap; + text-overflow: clip;*/ } diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index 7b31832698..bafdb5f01a 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -173,18 +173,21 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P // Update a group with a set of properties - .factory('UpdateGroup', [ function() { + .factory('UpdateGroup', ['ApplyEllipsis', function(ApplyEllipsis) { return function(params) { var scope = params.scope; var group_id = params.group_id; var properties = params.properties; // object of key:value pairs to update - + var old_name; for (var i=0; i < scope.groups.length; i++) { if (scope.groups[i].group_id == group_id) { var grp = scope.groups[i]; for (var p in properties) { - scope.groups[i][p] = properties[p]; + if (p == 'name') { + old_name = scope.groups[i].name; + } + scope.groups[i][p] = properties[p]; } } if (scope.groups[i].id == scope.selected_tree_id) { @@ -194,6 +197,15 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P scope.hostSearchPlaceholder = 'Search ' + scope.groups[i].name; } } + + // Update any titles attributes created by ApplyEllipsis + if (old_name) { + setTimeout(function() { + $('#groups_table .group-name a[title="' + old_name + '"').attr('title',properties.name); + ApplyEllipsis('#groups_table .group-name a'); + }, 2500); + } + } }]) diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index dfed22aea3..4d4ba92c40 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -539,18 +539,18 @@ angular.module('Utilities',['RestServices', 'Utilities']) }]) - /* Store - * - * Wrapper for local storage. All local storage requests flow through here so that we can - * stringify/unstringify objects and respond to future issues in one place. For example, - * we may at some point want to only use session storage rather than local storage. We might - * want to add a test for whether or not local/session storage exists for the browser, etc. - * - * store(key,value) will store the value using the key - * - * store(key) retrieves the value of the key - * - */ + /* Store + * + * Wrapper for local storage. All local storage requests flow through here so that we can + * stringify/unstringify objects and respond to future issues in one place. For example, + * we may at some point want to only use session storage rather than local storage. We might + * want to add a test for whether or not local/session storage exists for the browser, etc. + * + * store(key,value) will store the value using the key + * + * store(key) retrieves the value of the key + * + */ .factory('Store', ['Empty', function(Empty) { return function(key, value) { if (!Empty(value)) { @@ -562,8 +562,59 @@ angular.module('Utilities',['RestServices', 'Utilities']) var val = localStorage[key]; return (!Empty(val)) ? JSON.parse(val) : null; } - } - }]) + + } + }]) + + /* + * + * ApplyEllipsis() + * + */ + .factory('ApplyEllipsis', [ function() { + return function(selector) { + // Add a hidden element to the DOM. We'll use this to calc the px length of + // our target text. + var tmp = $('#string-test'); + if (!tmp.length) { + $('body').append(''); + tmp = $('#string-test'); + } + // Find and process the text. + $(selector).each(function() { + var setTitle = true; + var txt; + if ($(this).attr('title')) { + txt = $(this).attr('title'); + setTitle = false; + } + else { + txt = $(this).text(); + } + tmp.text(txt); + var w = tmp.width(); //text width + var pw = $(this).parent().width(); //parent width + if (w > pw) { + // text is wider than parent width + if (setTitle) { + // Save the original text in the title + $(this).attr('title',txt); + } + var cw = w / txt.length; // px width per character + var df = w - pw; // difference in px + txt = txt.substr(0, txt.length - (Math.ceil(df / cw) + 3)); + $(this).text(txt + '...'); + } + if (pw > w && !setTitle) { + // the parent has expanded and we previously set the title text + var txt = $(this).attr('title'); + $(this).text(txt); + } + }); + + } + }]); + diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index 0225526dfc..004e5deae3 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -582,7 +582,6 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job found = true; break; } - } return (found) ? false : true; } diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 9d29e82882..2253508c2a 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -471,9 +471,16 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) // Add collapse/expand icon --used on job_events page if (list['hasChildren'] && field.hasChildren) { - html += " " + - " "; - //ng-show=\"'\{\{ " + list.iterator + ".related.children \}\}' !== ''\" + html += " " + + " "; + //ng-show=\"'\{\{ " + list.iterator + ".related.children \}\}' !== ''\" + } + + if (list.name == 'groups') { + html += "
"; + } + if (list.name == 'hosts') { + html += "
"; } // Start the Link @@ -550,9 +557,14 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) && options.mode != 'lookup' && options.mode != 'select' && !field.noLink && !field.ngBindHtml ) { html += ""; } + + if (list.name == 'hosts' || list.name == 'groups') { + html += "
"; + } + // close ngShow html += (field.ngShow) ? "" : ""; - + // Specific to Job Events page -showing event detail/results html += (field.appendHTML) ? "
- AnsibleWorks AWX + Ansible Tower @@ -244,7 +244,7 @@
\n"; html += "\n"; html += "\n"; html += "\n"; @@ -90,8 +90,8 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H var job = jobs[j]; html += "\n"; html += "\n"; - html += "\n"; - html += "\n"; + html += "\n"; + html += "\n"; html += "\n"; } html += "\n"; diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index af723d1868..6e5ebd7426 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -7,7 +7,7 @@ * */ -angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinition']) +angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper']) .factory('JobStatusToolTip', [ function() { return function(status) { @@ -41,7 +41,8 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio }]) .factory('ShowJobSummary', ['Rest', 'Wait', 'GetBasePath', 'FormatDate', 'ProcessErrors', 'GenerateForm', 'JobSummary', - function(Rest, Wait, GetBasePath, FormatDate, ProcessErrors, GenerateForm, JobSummary) { + 'WatchInventoryWindowResize', + function(Rest, Wait, GetBasePath, FormatDate, ProcessErrors, GenerateForm, JobSummary, WatchInventoryWindowResize) { return function(params) { // Display status info in a modal dialog- called from inventory edit page @@ -109,6 +110,7 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio }); $('#status-modal-dialog').dialog('destroy'); $('#inventory-modal-container').empty(); + WatchInventoryWindowResize(); }, open: function(e, ui) { Wait('stop'); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 306d5288cb..80c1e8a33a 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -379,7 +379,7 @@ dd { .navbar>.container .navbar-brand { margin-left: 0; - margin-top: 10px; + margin-top: 11px; } /* Using inline-block rather than block keeps From 627305570c96e31387a53caf630c838cb0c77365 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 29 Jan 2014 13:39:40 -0500 Subject: [PATCH 15/22] AC-992 Fix inventory import tests. --- awx/main/tests/commands.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index e773ed3cbf..05dd37c665 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -462,12 +462,12 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(inventory_source.inventory_updates.count(), 1) inventory_update = inventory_source.inventory_updates.all()[0] self.assertEqual(inventory_update.status, 'successful') - for host in inventory.hosts.all(): + for host in inventory.hosts.filter(active=True): if host.pk in (except_host_pks or []): continue source_pks = host.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) - for group in inventory.groups.all(): + for group in inventory.groups.filter(active=True): if group.pk in (except_group_pks or []): continue source_pks = group.inventory_sources.values_list('pk', flat=True) @@ -619,20 +619,20 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): 'lbservers']) if overwrite: expected_group_names.remove('lbservers') - group_names = set(new_inv.groups.values_list('name', flat=True)) + group_names = set(new_inv.groups.filter(active=True).values_list('name', flat=True)) self.assertEqual(expected_group_names, group_names) expected_host_names = set(['web1.example.com', 'web2.example.com', 'web3.example.com', 'db1.example.com', 'db2.example.com', 'lb.example.com']) if overwrite: expected_host_names.remove('lb.example.com') - host_names = set(new_inv.hosts.values_list('name', flat=True)) + host_names = set(new_inv.hosts.filter(active=True).values_list('name', flat=True)) self.assertEqual(expected_host_names, host_names) expected_inv_vars = {'vara': 'A', 'varc': 'C'} if overwrite or overwrite_vars: expected_inv_vars.pop('varc') self.assertEqual(new_inv.variables_dict, expected_inv_vars) - for host in new_inv.hosts.all(): + for host in new_inv.hosts.filter(active=True): if host.name == 'web1.example.com': self.assertEqual(host.variables_dict, {'ansible_ssh_host': 'w1.example.net'}) @@ -642,35 +642,35 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(host.variables_dict, {'lbvar': 'ni!'}) else: self.assertEqual(host.variables_dict, {}) - for group in new_inv.groups.all(): + for group in new_inv.groups.filter(active=True): if group.name == 'servers': expected_vars = {'varb': 'B', 'vard': 'D'} if overwrite or overwrite_vars: expected_vars.pop('vard') self.assertEqual(group.variables_dict, expected_vars) - children = set(group.children.values_list('name', flat=True)) + children = set(group.children.filter(active=True).values_list('name', flat=True)) expected_children = set(['dbservers', 'webservers', 'lbservers']) if overwrite: expected_children.remove('lbservers') self.assertEqual(children, expected_children) - self.assertEqual(group.hosts.count(), 0) + self.assertEqual(group.hosts.filter(active=True).count(), 0) elif group.name == 'dbservers': self.assertEqual(group.variables_dict, {'dbvar': 'ugh'}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['db1.example.com','db2.example.com']) self.assertEqual(hosts, host_names) elif group.name == 'webservers': self.assertEqual(group.variables_dict, {'webvar': 'blah'}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['web1.example.com','web2.example.com', 'web3.example.com']) self.assertEqual(hosts, host_names) elif group.name == 'lbservers': self.assertEqual(group.variables_dict, {}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['lb.example.com']) self.assertEqual(hosts, host_names) if overwrite: From 9790715a250257cd8220ac29ac7e6a628acce587 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 29 Jan 2014 13:49:29 -0500 Subject: [PATCH 16/22] Update make develop command, version check after package rename. --- Makefile | 4 +++- awx/main/utils.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 02d71e5e5a..7863a6a6a6 100644 --- a/Makefile +++ b/Makefile @@ -72,12 +72,14 @@ requirements_pypi: sudo pip install -r requirements/dev.txt; \ fi -# "Install" awx package in development mode. Creates link to working +# "Install" ansible-tower package in development mode. Creates link to working # copy in site-packages and installs awx-manage command. develop: @if [ "$(VIRTUAL_ENV)" ]; then \ + pip uninstall -y awx; \ $(PYTHON) setup.py develop; \ else \ + sudo pip uninstall -y awx; \ sudo $(PYTHON) setup.py develop; \ fi diff --git a/awx/main/utils.py b/awx/main/utils.py index 88c11646f0..0ff157138e 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -77,7 +77,7 @@ def get_ansible_version(): def get_awx_version(): ''' - Return AWX version as reported by setuptools. + Return Ansible Tower version as reported by setuptools. ''' from awx import __version__ try: From 597a81ead65e7bd626c3f196daf88f3bceb49877 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 14:01:34 -0500 Subject: [PATCH 17/22] Fixing store references to point to http://www.ansible.com/ansible-pricing --- awx/ui/static/js/helpers/Access.js | 2 +- awx/ui/static/lib/ansible/license.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/helpers/Access.js b/awx/ui/static/js/helpers/Access.js index 26202a48e9..2c3d00bf29 100644 --- a/awx/ui/static/js/helpers/Access.js +++ b/awx/ui/static/js/helpers/Access.js @@ -50,7 +50,7 @@ angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies']) var license = $cookieStore.get('license'); var purchase_msg = '

To purchase a license or extend an existing license ' + - 'visit the Ansible online store, ' + + 'visit the Ansible online store, ' + 'or visit support.ansible.com for assistance.

'; if (license && !Authorization.licenseTested()) { diff --git a/awx/ui/static/lib/ansible/license.js b/awx/ui/static/lib/ansible/license.js index cacba6b0ce..ebf08eec95 100644 --- a/awx/ui/static/lib/ansible/license.js +++ b/awx/ui/static/lib/ansible/license.js @@ -131,7 +131,7 @@ angular.module('License', ['RestServices', 'Utilities', 'FormGenerator', 'Prompt Prompt({ hdr: 'Tower Licensing', body: "

Ansible Tower licenses can be purchased or extended by visiting " + + "href=\"http://www.ansible.com/ansible-pricing\" target=\"_blank\">" + "the Ansible online store. Would you like to purchase or extend your license now?

", 'class': 'btn-primary', action: function() { From 57235cad7a65cd84dcb65e314276a0cbc849e1b7 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 29 Jan 2014 14:04:31 -0500 Subject: [PATCH 18/22] Remove 'awx-' prefix when reporting version --- awx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/__init__.py b/awx/__init__.py index 6a23bcf9b0..5633508baa 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -71,6 +71,6 @@ def manage(): # Now run the command (or display the version). from django.core.management import execute_from_command_line if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): - sys.stdout.write('awx-%s\n' % __version__) + sys.stdout.write('%s\n' % __version__) else: execute_from_command_line(sys.argv) From 168c4235f51e617a4858663b7be6c7d9035822b6 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 16:59:52 -0500 Subject: [PATCH 19/22] AC-977 Fixed raw html showing up in Action field on detail dialog. Added a subtitle on the AS widget. On inventory groups/host page the subtitle explains what activities are being displayed, and only activities realted to the selected group (on the inventory page) are displayed. If All Hosts selected, then all activities for all groups related to the current inventory are displayed. Also, brought back activity stream button for inventory hosts. --- awx/ui/static/js/controllers/Inventories.js | 20 ++++++++++++++++++-- awx/ui/static/js/lists/InventoryHosts.js | 8 +++++++- awx/ui/static/js/widgets/Stream.js | 19 +++++++++++++++---- awx/ui/static/less/ansible-ui.less | 5 +++++ awx/ui/static/lib/ansible/list-generator.js | 9 +++++++++ 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 44208ce494..e976465537 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -521,10 +521,26 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis } $scope.showGroupActivity = function() { - var url = GetBasePath('activity_stream') + '?group__inventory__id=' + $scope.inventory_id; - Stream({ scope: $scope, inventory_name: $scope.inventory_name, url: url }); + var url, title, group; + if ($scope.selected_group_id) { + group = Find({ list: $scope.groups, key: 'id', val: $scope.selected_tree_id }); + url = GetBasePath('activity_stream') + '?group__id=' + $scope.selected_group_id; + title = 'Showing all activities for group ' + group.name; + } + else { + title = 'Showing all activities for all ' + $scope.inventory_name + ' groups'; + url = GetBasePath('activity_stream') + '?group__inventory__id=' + $scope.inventory_id; + } + Stream({ scope: $scope, inventory_name: $scope.inventory_name, url: url, title: title }); } + $scope.showHostActivity = function() { + var url, title; + title = 'Showing all activities for all ' + $scope.inventory_name + ' hosts'; + url = GetBasePath('activity_stream') + '?host__inventory__id=' + $scope.inventory_id; + Stream({ scope: $scope, inventory_name: $scope.inventory_name, url: url, title: title }); + } + $scope.showJobSummary = function(job_id) { ShowJobSummary({ job_id: job_id }); } diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index 3efae93e01..b40cd9ea6b 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -100,7 +100,13 @@ angular.module('InventoryHostsDefinition', []) ngHide: 'selected_tree_id == 1', //disable when 'All Hosts' selected awToolTip: "Create a new host" }, - help: { + stream: { + ngClick: "showHostActivity()", + awToolTip: "View Activity Stream", + mode: 'all', + ngShow: "user_is_superuser" + }, + help: { mode: 'all', awToolTip: //"
" + diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index 7bef535ed8..25baf67b82 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -189,14 +189,17 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } else if (obj1) { name = ''; + var name_nolink = ''; // find the name in changes, if needed if ( !(obj1_obj && obj1_obj.name) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name) ) { if (activity.changes && activity.changes.name) { if (typeof activity.changes.name == 'string') { name = ' ' + activity.changes.name; + name_nolink = name; } else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { - name = ' ' + activity.changes.name[0] + name = ' ' + activity.changes.name[0]; + name_nolink = name; } } else if (obj1 == 'job' && obj1_obj && activity.changes && activity.changes.job_template) { @@ -204,9 +207,11 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti if (activity.operation != 'delete') { obj1_obj['base'] = obj1; name = ' ' + ''+ obj1_obj.id + ' ' + activity.changes.job_template + ''; + name_nolink = ' ' + obj1_obj.id + ' ' + activity.changes.job_template; } else { name = ' ' + obj1_obj.id + ' ' + activity.changes.job_template; + name_nolink = name; } } else if (obj1 == 'job' && obj1_obj) { @@ -214,17 +219,20 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti if (activity.operation != 'delete') { obj1_obj['base'] = obj1; name = ' ' + '' + obj1_obj.id + ''; + name_nolink = ' ' + obj1_obj.id; } else { name = ' ' + obj1_obj.id; + name_nolink = name; } } } else if (obj1_obj && obj1_obj.name) { name = ' ' + stripDeleted(obj1_obj.name); + name_nolink = name; } descr += obj1 + name; - descr_nolink += obj1 + name; + descr_nolink += obj1 + name_nolink; } activity['description'] = descr; activity['description_nolink'] = descr_nolink; @@ -301,10 +309,10 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti var PreviousSearchParams = Store('CurrentSearchParams'); // pass in an inventory name to fix breadcrumb display - var inventory_name = (params) ? params.inventory_name : null; + var inventory_name = (params && params.inventory_name) ? params.inventory_name : null; // url will override the attempt to compute an activity_stream query - var url = (params) ? params.url : null; + var url = (params && params.url) ? params.url : null; $rootScope.flashMessage = null; @@ -357,6 +365,9 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti secondWidget: true, activityStream: true }); + + // descriptive title describing what AS is showing + scope.streamTitle = (params && params.title) ? params.title : null; scope.closeStream = function(inUrl) { HideStream(); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 80c1e8a33a..6774c9db81 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1382,6 +1382,11 @@ tr td button i { border: 1px solid @grey; border-radius: 8px; padding: 8px; + + h5 { + margin-top: 0; + margin-bottom: 20px; + } } /* diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index bc0951eafa..5c84c2be82 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -147,6 +147,15 @@ angular.module('ListGenerator', ['GeneratorHelpers']) html += "
\n"; } + if (options.activityStream) { + // Add a title row + html += "
\n"; + html += "
\n"; + html += "
{{ streamTitle }}
\n"; + html += "
\n"; + html += "
\n"; + } + html += "
\n"; if (list.name != 'groups') { From 9d8c124817fa1d34213ea30d411106b9bc9b011e Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 17:06:53 -0500 Subject: [PATCH 20/22] Fixed AS widget breadcrumb links --- awx/ui/static/lib/ansible/list-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index 5c84c2be82..4d23667681 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -126,7 +126,7 @@ angular.module('ListGenerator', ['GeneratorHelpers']) // before navigation html += "
\n"; html += "\n
\n"; From 82aa0ac85dcdd9151937a28bbbbc6e33f07719da Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 18:19:13 -0500 Subject: [PATCH 21/22] fix for the small viewport issue (https://www.dropbox.com/s/dmf4aixfv7k67cm/Screenshot%202014-01-29%2015.19.52.png) where group name div gets pushed to a new line. --- awx/ui/static/less/ansible-ui.less | 61 ++++++++---------------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 6774c9db81..2b729b0765 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1442,35 +1442,15 @@ tr td button i { @media (min-width: 768px) and (max-width: 1199px) { .list-actions button, .list-actions .checkbox-inline { - margin-top: 10px; + margin-top: 10px; } - /*.tree-form-container { - padding-left: 15px; - padding-right: 15px; - - } - - .tree-view-container .col-lg-4 { - padding-right: 15px; - } - - .tree-controls .btn-container { - padding-left: 15px; - } - - #tree-view { - margin-left: 0; - margin-top: 10px; - overflow: hidden; - } - - #tree-form { - margin-top: 10px; - }*/ - .label-text { - text-align: left; + text-align: left; + } + + .group-name { + width: 80%; } } @@ -1486,43 +1466,34 @@ tr td button i { .level-2, .level-3, .level-3-detail { - padding-left: 0; + padding-left: 0; } table { - word-wrap: break-word; - table-layout: fixed; + word-wrap: break-word; + table-layout: fixed; } th.actions-column, td.actions { - white-space: normal; + white-space: normal; } td.actions .btn { - width: 75px; - margin-bottom: 5px; + width: 75px; + margin-bottom: 5px; } .list-actions button, .list-actions .checkbox-inline { - margin-top: 10px; + margin-top: 10px; } - .tree-form-container { - padding-left: 15px; - padding-right: 15px; - } - - .tree-view-container .col-lg-4 { - padding-right: 15px; - } - - .tree-controls .btn-container { - padding-left: 15px; + .group-name { + width: 80%; } .label-text { - text-align: left; + text-align: left; } } From e2aef64a4d963a61e4dd595ee79120afa8572468 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 19:58:34 -0500 Subject: [PATCH 22/22] AC-995 fixed typo and added extra error handling in case we actually cannot parse inventory variables. If that does happen, we now alert the user and keep the application stable and running. --- awx/ui/static/js/helpers/inventory.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index d88f5132e5..7c0b446015 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -137,12 +137,24 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi if (fld == 'inventory_variables') { // Parse variables, converting to YAML. if ($.isEmptyObject(data.variables) || data.variables == "\{\}" || - data.variables == "null" || data.data_variables == "") { + data.variables == "null" || data.variables == "") { scope.inventory_variables = "---"; } else { - var json_obj = JSON.parse(data.variables); - scope.inventory_variables = jsyaml.safeDump(json_obj); + try { + var json_obj = JSON.parse(data.variables); + scope.inventory_variables = jsyaml.safeDump(json_obj); + } + catch(err) { + Alert('Variable Parse Error', 'Attempted to parse variables for inventory: ' + inventory_id + + '. Parse returned: ' + err); + if (console) { + console.log(err); + console.log('data:'); + console.log(data.variables); + } + scope.inventory_variables = '---'; + } } master.inventory_variables = scope.variables; }
ID\n"; - html += "Status\n"; - html += "Name\n"; + html += "Status\n"; + html += "Name\n"; html += "
" + job.id + " " + job.status + "" + job.name + "" + job.id + "" + job.name + "
ID\n"; html += "Status\n"; - html += "Name\n"; + html += "Name\n"; html += "
" + job.id + "" + job.name + "" + job.name + "