From 0d047ef69473ce74cdc4702172fdf01b47e4978e Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Jun 2017 09:28:39 -0400 Subject: [PATCH 01/69] Implementing containerized tower packaging --- Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9b48cb4964..4c004017e7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON = python +PYTHON ?= python PYTHON_VERSION = $(shell $(PYTHON) -c "from distutils.sysconfig import get_python_version; print(get_python_version())") SITELIB=$(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") OFFICIAL ?= no @@ -73,7 +73,7 @@ else SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)-$(RELEASE) SDIST_TAR_NAME=$(NAME)-$(VERSION)-$(RELEASE) endif -SDIST_TAR_FILE=$(SDIST_TAR_NAME).tar.gz +SDIST_TAR_FILE ?= $(SDIST_TAR_NAME).tar.gz SETUP_TAR_FILE=$(SETUP_TAR_NAME).tar.gz SETUP_TAR_LINK=$(NAME)-setup-latest.tar.gz SETUP_TAR_CHECKSUM=$(NAME)-setup-CHECKSUM @@ -674,6 +674,9 @@ release_clean: dist/$(SDIST_TAR_FILE): ui-release BUILD="$(BUILD)" $(PYTHON) setup.py sdist +dist/ansible-tower.tar.gz: ui-release + OFFICIAL="yes" $(PYTHON) setup.py sdist + sdist: dist/$(SDIST_TAR_FILE) @echo "#############################################" @echo "Artifacts:" @@ -972,3 +975,7 @@ clean-elk: psql-container: docker run -it --net tools_default --rm postgres:9.4.1 sh -c 'exec psql -h "postgres" -p "5432" -U postgres' + +docker-production-build: dist/ansible-tower.tar.gz + docker build -t ansible/tower_web -f packaging/docker/Dockerfile . + docker build -t ansible/tower_task -f packaging/docker/Dockerfile.celery . From ad4a7fe033315f083d138f9e21b59f42e1f56f0e Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sat, 24 Jun 2017 12:35:11 -0400 Subject: [PATCH 02/69] access_registry made simple dict with auto registration --- awx/main/access.py | 100 +++++------------- awx/main/models/workflow.py | 19 ++-- awx/main/tests/conftest.py | 2 +- .../functional/api/test_rbac_displays.py | 10 +- 4 files changed, 40 insertions(+), 91 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 6ce7600026..7b7ede48e4 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -31,7 +31,7 @@ __all__ = ['get_user_queryset', 'check_user_access', 'check_user_access_with_err logger = logging.getLogger('awx.main.access') access_registry = { - # : [, ...], + # : , # ... } @@ -41,8 +41,7 @@ class StateConflict(ValidationError): def register_access(model_class, access_class): - access_classes = access_registry.setdefault(model_class, []) - access_classes.append(access_class) + access_registry[model_class] = access_class @property @@ -66,19 +65,9 @@ def get_user_queryset(user, model_class): Return a queryset for the given model_class containing only the instances that should be visible to the given user. ''' - querysets = [] - for access_class in access_registry.get(model_class, []): - access_instance = access_class(user) - querysets.append(access_instance.get_queryset()) - if not querysets: - return model_class.objects.none() - elif len(querysets) == 1: - return querysets[0] - else: - queryset = model_class.objects.all() - for qs in querysets: - queryset = queryset.filter(pk__in=qs.values_list('pk', flat=True)) - return queryset + access_class = access_registry[model_class] + access_instance = access_class(user) + return access_instance.get_queryset() def check_user_access(user, model_class, action, *args, **kwargs): @@ -86,33 +75,26 @@ def check_user_access(user, model_class, action, *args, **kwargs): Return True if user can perform action against model_class with the provided parameters. ''' - for access_class in access_registry.get(model_class, []): - access_instance = access_class(user) - access_method = getattr(access_instance, 'can_%s' % action, None) - if not access_method: - logger.debug('%s.%s not found', access_instance.__class__.__name__, - 'can_%s' % action) - continue - result = access_method(*args, **kwargs) - logger.debug('%s.%s %r returned %r', access_instance.__class__.__name__, - getattr(access_method, '__name__', 'unknown'), args, result) - if result: - return result - return False + access_class = access_registry[model_class] + access_instance = access_class(user) + access_method = getattr(access_instance, 'can_%s' % action) + result = access_method(*args, **kwargs) + logger.debug('%s.%s %r returned %r', access_instance.__class__.__name__, + getattr(access_method, '__name__', 'unknown'), args, result) + return result def check_user_access_with_errors(user, model_class, action, *args, **kwargs): ''' Return T/F permission and summary of problems with the action. ''' - for access_class in access_registry.get(model_class, []): - access_instance = access_class(user, save_messages=True) - access_method = getattr(access_instance, 'can_%s' % action, None) - result = access_method(*args, **kwargs) - logger.debug('%s.%s %r returned %r', access_instance.__class__.__name__, - access_method.__name__, args, result) - return (result, access_instance.messages) - return (False, '') + access_class = access_registry[model_class] + access_instance = access_class(user, save_messages=True) + access_method = getattr(access_instance, 'can_%s' % action, None) + result = access_method(*args, **kwargs) + logger.debug('%s.%s %r returned %r', access_instance.__class__.__name__, + access_method.__name__, args, result) + return (result, access_instance.messages) def get_user_capabilities(user, instance, **kwargs): @@ -123,9 +105,8 @@ def get_user_capabilities(user, instance, **kwargs): convenient for the user interface to consume and hide or show various actions in the interface. ''' - for access_class in access_registry.get(type(instance), []): - return access_class(user).get_user_capabilities(instance, **kwargs) - return None + access_class = access_registry[instance.__class__] + return access_class(user).get_user_capabilities(instance, **kwargs) def check_superuser(func): @@ -2008,7 +1989,7 @@ class UnifiedJobTemplateAccess(BaseAccess): return qs.all() def can_start(self, obj, validate_license=True): - access_class = access_registry.get(obj.__class__, [])[0] + access_class = access_registry[obj.__class__] access_instance = access_class(self.user) return access_instance.can_start(obj, validate_license=validate_license) @@ -2376,38 +2357,5 @@ class RoleAccess(BaseAccess): return False -register_access(User, UserAccess) -register_access(Organization, OrganizationAccess) -register_access(Inventory, InventoryAccess) -register_access(Host, HostAccess) -register_access(Group, GroupAccess) -register_access(InventorySource, InventorySourceAccess) -register_access(InventoryUpdate, InventoryUpdateAccess) -register_access(Credential, CredentialAccess) -register_access(CredentialType, CredentialTypeAccess) -register_access(Team, TeamAccess) -register_access(Project, ProjectAccess) -register_access(ProjectUpdate, ProjectUpdateAccess) -register_access(JobTemplate, JobTemplateAccess) -register_access(Job, JobAccess) -register_access(JobHostSummary, JobHostSummaryAccess) -register_access(JobEvent, JobEventAccess) -register_access(SystemJobTemplate, SystemJobTemplateAccess) -register_access(SystemJob, SystemJobAccess) -register_access(AdHocCommand, AdHocCommandAccess) -register_access(AdHocCommandEvent, AdHocCommandEventAccess) -register_access(Schedule, ScheduleAccess) -register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess) -register_access(UnifiedJob, UnifiedJobAccess) -register_access(ActivityStream, ActivityStreamAccess) -register_access(CustomInventoryScript, CustomInventoryScriptAccess) -register_access(Role, RoleAccess) -register_access(NotificationTemplate, NotificationTemplateAccess) -register_access(Notification, NotificationAccess) -register_access(Label, LabelAccess) -register_access(WorkflowJobTemplateNode, WorkflowJobTemplateNodeAccess) -register_access(WorkflowJobNode, WorkflowJobNodeAccess) -register_access(WorkflowJobTemplate, WorkflowJobTemplateAccess) -register_access(WorkflowJob, WorkflowJobAccess) -register_access(Instance, InstanceAccess) -register_access(InstanceGroup, InstanceGroupAccess) +for cls in BaseAccess.__subclasses__(): + access_registry[cls.model] = cls diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b5a3afba1d..52d58717a0 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -187,15 +187,16 @@ class WorkflowJobTemplateNode(WorkflowNodeBase): ''' create_kwargs = {} for field_name in self._get_workflow_job_field_names(): - if hasattr(self, field_name): - item = getattr(self, field_name) - if field_name in ['inventory', 'credential']: - if not user.can_access(item.__class__, 'use', item): - continue - if field_name in ['unified_job_template']: - if not user.can_access(item.__class__, 'start', item, validate_license=False): - continue - create_kwargs[field_name] = item + item = getattr(self, field_name, None) + if item is None: + continue + if field_name in ['inventory', 'credential']: + if not user.can_access(item.__class__, 'use', item): + continue + if field_name in ['unified_job_template']: + if not user.can_access(item.__class__, 'start', item, validate_license=False): + continue + create_kwargs[field_name] = item create_kwargs['workflow_job_template'] = workflow_job_template return self.__class__.objects.create(**create_kwargs) diff --git a/awx/main/tests/conftest.py b/awx/main/tests/conftest.py index 35f77c1f1d..6118bf83bd 100644 --- a/awx/main/tests/conftest.py +++ b/awx/main/tests/conftest.py @@ -24,7 +24,7 @@ def mock_access(): mock_instance = mock.MagicMock(__name__='foobar') MockAccess = mock.MagicMock(return_value=mock_instance) the_patch = mock.patch.dict('awx.main.access.access_registry', - {TowerClass: [MockAccess]}, clear=False) + {TowerClass: MockAccess}, clear=False) the_patch.__enter__() yield mock_instance finally: diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 99723c545f..ca83d6df4a 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -188,7 +188,7 @@ class TestAccessListCapabilities: self, inventory, rando, get, mocker, mock_access_method): inventory.admin_role.members.add(rando) - with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + with mocker.patch.object(access_registry[Role], 'can_unattach', mock_access_method): response = get(reverse('api:inventory_access_list', kwargs={'pk': inventory.id}), rando) mock_access_method.assert_called_once_with(inventory.admin_role, rando, 'members', **self.extra_kwargs) @@ -198,7 +198,7 @@ class TestAccessListCapabilities: def test_access_list_indirect_access_capability( self, inventory, organization, org_admin, get, mocker, mock_access_method): - with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + with mocker.patch.object(access_registry[Role], 'can_unattach', mock_access_method): response = get(reverse('api:inventory_access_list', kwargs={'pk': inventory.id}), org_admin) mock_access_method.assert_called_once_with(organization.admin_role, org_admin, 'members', **self.extra_kwargs) @@ -210,7 +210,7 @@ class TestAccessListCapabilities: self, inventory, team, team_member, get, mocker, mock_access_method): team.member_role.children.add(inventory.admin_role) - with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + with mocker.patch.object(access_registry[Role], 'can_unattach', mock_access_method): response = get(reverse('api:inventory_access_list', kwargs={'pk': inventory.id}), team_member) mock_access_method.assert_called_once_with(inventory.admin_role, team.member_role, 'parents', **self.extra_kwargs) @@ -229,7 +229,7 @@ class TestAccessListCapabilities: def test_team_roles_unattach(mocker, team, team_member, inventory, mock_access_method, get): team.member_role.children.add(inventory.admin_role) - with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + with mocker.patch.object(access_registry[Role], 'can_unattach', mock_access_method): response = get(reverse('api:team_roles_list', kwargs={'pk': team.id}), team_member) # Did we assess whether team_member can remove team's permission to the inventory? @@ -244,7 +244,7 @@ def test_user_roles_unattach(mocker, organization, alice, bob, mock_access_metho organization.member_role.members.add(alice) organization.member_role.members.add(bob) - with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + with mocker.patch.object(access_registry[Role], 'can_unattach', mock_access_method): response = get(reverse('api:user_roles_list', kwargs={'pk': alice.id}), bob) # Did we assess whether bob can remove alice's permission to the inventory? From 3df6cda4cd5c3018c878fd336b36fd55a7681341 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 29 Jun 2017 15:39:36 -0400 Subject: [PATCH 03/69] add logging protocol and timeout to ctit ui --- .../src/configuration/configuration.controller.js | 6 +++++- .../system-form/configuration-system.controller.js | 5 +++++ .../system-form/sub-forms/system-logging.form.js | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index d68c22e9b4..33056ec486 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -392,7 +392,11 @@ export default [ } else { // Everything else - payload[key] = $scope[key]; + if (key !== 'LOG_AGGREGATOR_TCP_TIMEOUT' || + ($scope.LOG_AGGREGATOR_PROTOCOL === 'https' || + $scope.LOG_AGGREGATOR_PROTOCOL === 'tcp')) { + payload[key] = $scope[key]; + } } }); diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index bf31b99881..51d6f8ebaf 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -171,6 +171,10 @@ export default [ $scope.$parent.LOG_AGGREGATOR_TYPE = _.find($scope.$parent.LOG_AGGREGATOR_TYPE_options, { value: $scope.$parent.LOG_AGGREGATOR_TYPE }); } + if($scope.$parent.LOG_AGGREGATOR_PROTOCOL !== null) { + $scope.$parent.LOG_AGGREGATOR_PROTOCOL = _.find($scope.$parent.LOG_AGGREGATOR_PROTOCOL_options, { value: $scope.$parent.LOG_AGGREGATOR_PROTOCOL }); + } + if(flag !== undefined){ dropdownRendered = flag; } @@ -183,6 +187,7 @@ export default [ placeholder: i18n._('Select types'), }); $scope.$parent.configuration_logging_template_form.LOG_AGGREGATOR_TYPE.$setPristine(); + $scope.$parent.configuration_logging_template_form.LOG_AGGREGATOR_PROTOCOL.$setPristine(); } } diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js index 5763e450b5..386c8f708d 100644 --- a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js @@ -42,6 +42,20 @@ }, LOG_AGGREGATOR_ENABLED: { type: 'toggleSwitch', + }, + LOG_AGGREGATOR_PROTOCOL: { + type: 'select', + reset: 'LOG_AGGREGATOR_PROTOCOL', + ngOptions: 'type.label for type in LOG_AGGREGATOR_PROTOCOL_options track by type.value' + }, + LOG_AGGREGATOR_TCP_TIMEOUT: { + type: 'text', + reset: 'LOG_AGGREGATOR_TCP_TIMEOUT', + ngShow: 'LOG_AGGREGATOR_PROTOCOL.value === "tcp" || LOG_AGGREGATOR_PROTOCOL.value === "https"', + awRequiredWhen: { + reqExpression: "LOG_AGGREGATOR_PROTOCOL.value === 'tcp' || LOG_AGGREGATOR_PROTOCOL.value === 'https'", + init: "false" + }, } }, From 611c42f7410f416ce614fc0d8ba7bb85f8e097f7 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 29 Jun 2017 15:40:09 -0400 Subject: [PATCH 04/69] fix the `make rdb` debugging tool --- Makefile | 11 +++++------ tools/rdb.py | 7 ++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 00fd4dc9e0..efcaeb0270 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,6 @@ NODE ?= node NPM_BIN ?= npm DEPS_SCRIPT ?= packaging/bundle/deps.py GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) -DOCKER_HOST_IP=`python -c "import socket; print(socket.gethostbyname(socket.gethostname()))"` GCLOUD_AUTH ?= $(shell gcloud auth print-access-token) # NOTE: This defaults the container image version to the branch that's active @@ -953,13 +952,13 @@ docker-isolated: # Docker Compose Development environment docker-compose: docker-auth - DOCKER_HOST_IP=$(DOCKER_HOST_IP) TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose.yml up --no-recreate tower + TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose.yml up --no-recreate tower docker-compose-cluster: docker-auth - DOCKER_HOST_IP=$(DOCKER_HOST_IP) TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose-cluster.yml up + TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose-cluster.yml up docker-compose-test: docker-auth - cd tools && DOCKER_HOST_IP=$(DOCKER_HOST_IP) TAG=$(COMPOSE_TAG) docker-compose run --rm --service-ports tower /bin/bash + cd tools && TAG=$(COMPOSE_TAG) docker-compose run --rm --service-ports tower /bin/bash docker-compose-build: tower-devel-build tower-isolated-build @@ -983,10 +982,10 @@ docker-refresh: docker-clean docker-compose # Docker Development Environment with Elastic Stack Connected docker-compose-elk: docker-auth - DOCKER_HOST_IP=$(DOCKER_HOST_IP) TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate + TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate docker-compose-cluster-elk: docker-auth - DOCKER_HOST_IP=$(DOCKER_HOST_IP) TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose-cluster.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate + TAG=$(COMPOSE_TAG) docker-compose -f tools/docker-compose-cluster.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate clean-elk: docker stop tools_kibana_1 diff --git a/tools/rdb.py b/tools/rdb.py index 9552c5317e..6670805ea8 100644 --- a/tools/rdb.py +++ b/tools/rdb.py @@ -155,9 +155,14 @@ class CustomPdb(Rdb): return Rdb.displayhook(self, obj) def get_avail_port(self, *args, **kwargs): + try: + socket.gethostbyname('docker.for.mac.localhost') + host = 'docker.for.mac.localhost' + except: + host = os.popen('ip route').read().split(' ')[2] sock, port = Rdb.get_avail_port(self, *args, **kwargs) socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto( - str(port), ('dockerhost', 6899) + str(port), (host, 6899) ) return (sock, port) From 9aa4ea8e614b18b6a99e674cb1ad04dcc6baaa2c Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Wed, 21 Jun 2017 12:24:43 -0400 Subject: [PATCH 05/69] Add boto3 dependency and remove requests freezing --- requirements/README.md | 2 + requirements/requirements.in | 2 +- requirements/requirements.txt | 96 ++++++++++++----------- requirements/requirements_ansible.in | 2 +- requirements/requirements_ansible.txt | 73 +++++++++-------- requirements/requirements_ansible_git.txt | 1 + requirements/requirements_git.txt | 1 + 7 files changed, 96 insertions(+), 81 deletions(-) diff --git a/requirements/README.md b/requirements/README.md index 3511ea5410..a55eb998e2 100644 --- a/requirements/README.md +++ b/requirements/README.md @@ -21,3 +21,5 @@ pip-compile requirements/requirements_ansible.in > requirements/requirements_ans * all dependencies are NOT captured in our `.txt` files. This means you can't rely on the `.txt` when gathering licenses. * Packages `gevent-websocket` and `twisted` are put in `requirements.in` *not* because they are primary dependency of Tower, but because their versions needs to be freezed as dependencies of django channel. Please be mindful when doing dependency updates. + +* Package `docutils`, as an upstream of `boto3`, is commented out in both `requirements.txt` and `requirements_ansible.txt` because the official package has a bug that causes RPM build failure. [Here](https://sourceforge.net/p/docutils/bugs/321/) is the bug report. Please do not uncomment it before the bug fix lands. For now we are using [a monkey-patch version of `docutils`](https://github.com/ansible/docutils.git) that comes with the bug fix. It's included in `requirements_git.txt` and `requirements_ansible_git.txt`. diff --git a/requirements/requirements.in b/requirements/requirements.in index ec12af5c68..a178a9e8a1 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -5,6 +5,7 @@ asgiref==1.0.1 azure==2.0.0rc6 backports.ssl-match-hostname==3.5.0.1 boto==2.46.1 +boto3==1.4.4 channels==0.17.3 celery==3.1.25 daphne>=0.15.0,<1.0.0 @@ -41,7 +42,6 @@ python-saml==2.2.1 python-social-auth==0.2.21 pyvmomi==6.5 redbaron==0.6.3 -requests==2.11.1 requests-futures==0.9.7 service-identity==16.0.0 shade==1.20.0 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0ab505857c..b2d3ea2dea 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -12,8 +12,8 @@ appdirs==1.4.2 asgi-amqp==0.4.1 asgiref==1.0.1 asn1crypto==0.22.0 # via cryptography -attrs==16.3.0 # via service-identity -autobahn==17.5.1 # via daphne +attrs==17.2.0 # via service-identity +autobahn==17.6.1 # via daphne azure-batch==1.0.0 # via azure azure-common[autorest]==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage azure-mgmt-batch==1.0.0 # via azure-mgmt @@ -33,21 +33,23 @@ azure-servicemanagement-legacy==0.20.4 # via azure azure-storage==0.33.0 # via azure azure==2.0.0rc6 babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-neutronclient, python-novaclient, python-openstackclient -backports.functools-lru-cache==1.3 # via jaraco.functools +backports.functools-lru-cache==1.4 # via jaraco.functools backports.ssl-match-hostname==3.5.0.1 -baron==0.6.5 # via redbaron +baron==0.6.6 # via redbaron billiard==3.3.0.23 # via celery +boto3==1.4.4 boto==2.46.1 +botocore==1.5.72 # via boto3, s3transfer celery==3.1.25 #certifi==2017.4.17 # via msrest cffi==1.10.0 # via cryptography channels==0.17.3 -cliff==2.6.0 # via osc-lib, python-designateclient, python-neutronclient, python-openstackclient -cmd2==0.7.0 # via cliff +cliff==2.7.0 # via osc-lib, python-designateclient, python-neutronclient, python-openstackclient +cmd2==0.7.2 # via cliff constantly==15.1.0 # via twisted -cryptography==1.8.1 # via adal, azure-storage, pyopenssl, secretstorage, twilio +cryptography==1.9 # via adal, azure-storage, pyopenssl, secretstorage, twilio daphne==0.15.0 -debtcollector==1.13.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient +debtcollector==1.15.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient decorator==4.0.11 # via shade defusedxml==0.4.1 # via python-saml deprecation==1.0.1 # via openstacksdk @@ -65,16 +67,17 @@ django-transaction-hooks==0.2 django==1.8.16 # via channels, django-auth-ldap, django-crum, django-split-settings, django-transaction-hooks djangorestframework-yaml==1.0.3 djangorestframework==3.3.3 -dogpile.cache==0.6.2 # via python-ironicclient, shade +#docutils==0.12 # via botocore +dogpile.cache==0.6.3 # via python-ironicclient, shade enum34==1.1.6 # via cryptography, msrest funcsigs==1.0.2 # via debtcollector, oslo.utils functools32==3.2.3.post2 # via jsonschema -futures==3.1.1 # via azure-storage, requests-futures, shade +futures==3.1.1 # via azure-storage, requests-futures, s3transfer, shade gevent-websocket==0.9.5 -gevent==1.2.1 # via gevent-websocket +gevent==1.2.2 # via gevent-websocket greenlet==0.4.12 # via gevent idna==2.5 # via cryptography, twilio -incremental==16.10.1 # via twisted +incremental==17.5.0 # via twisted inflect==0.2.5 # via jaraco.itertools ipaddress==1.0.18 # via cryptography, shade irc==15.1.1 @@ -87,36 +90,36 @@ jaraco.itertools==2.0.1 # via irc jaraco.logging==1.5 # via irc jaraco.stream==1.1.2 # via irc jaraco.text==1.9.2 # via irc, jaraco.collections -jmespath==0.9.2 # via shade -jsonpatch==1.15 # via shade, warlock +jmespath==0.9.3 # via boto3, botocore, shade +jsonpatch==1.16 # via openstacksdk, shade, warlock jsonpickle==0.9.4 # via asgi-amqp jsonpointer==1.10 # via jsonpatch jsonschema==2.6.0 -keyring==10.3.2 # via msrestazure -keystoneauth1==2.20.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, shade +keyring==10.3.3 # via msrestazure +keystoneauth1==2.21.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, shade kombu==3.0.37 # via asgi-amqp, celery -lxml==3.7.3 +lxml==3.8.0 # via pyvmomi m2crypto==0.25.1 markdown==2.6.7 monotonic==1.3 # via oslo.utils -more-itertools==3.0.0 # via irc, jaraco.functools, jaraco.itertools +more-itertools==3.2.0 # via irc, jaraco.functools, jaraco.itertools msgpack-python==0.4.8 # via asgi-amqp, oslo.serialization -msrest==0.4.7 # via azure-common, msrestazure -msrestazure==0.4.7 # via azure-common +msrest==0.4.10 # via azure-common, msrestazure +msrestazure==0.4.9 # via azure-common munch==2.1.1 # via shade netaddr==0.7.19 # via oslo.config, oslo.utils, pyrad, python-neutronclient -netifaces==0.10.5 # via oslo.utils, shade +netifaces==0.10.6 # via oslo.utils, shade oauthlib==2.0.2 # via python-social-auth, requests-oauthlib -openstacksdk==0.9.16 # via python-openstackclient +openstacksdk==0.9.17 # via python-openstackclient ordereddict==1.1 os-client-config==1.27.0 # via openstacksdk, osc-lib, python-neutronclient, shade osc-lib==1.6.0 # via python-designateclient, python-ironicclient, python-neutronclient, python-openstackclient -oslo.config==4.1.0 # via python-keystoneclient -oslo.i18n==3.15.0 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient +oslo.config==4.6.0 # via python-keystoneclient +oslo.i18n==3.15.3 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient oslo.serialization==2.18.0 # via python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient -oslo.utils==3.25.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient -packaging==16.8 # via cryptography, setuptools -pbr==3.0.0 # via cliff, debtcollector, keystoneauth1, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, requestsexceptions, shade, stevedore +oslo.utils==3.26.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient +packaging==16.8 # via setuptools +pbr==3.1.1 # via cliff, debtcollector, keystoneauth1, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, requestsexceptions, shade, stevedore pexpect==4.2.1 positional==1.1.1 # via keystoneauth1, python-keystoneclient prettytable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-ironicclient, python-novaclient @@ -124,25 +127,25 @@ psphere==0.5.2 psutil==5.2.2 psycopg2==2.7.1 ptyprocess==0.5.1 # via pexpect -pyasn1-modules==0.0.8 # via service-identity +pyasn1-modules==0.0.9 # via service-identity pyasn1==0.2.3 # via pyasn1-modules, service-identity pycparser==2.17 # via cffi pygerduty==0.35.2 pyjwt==1.5.0 # via adal, python-social-auth, twilio -pyopenssl==17.0.0 # via service-identity, twilio +pyopenssl==17.0.0 # via pyvmomi, service-identity, twilio pyparsing==2.2.0 pyrad==2.1 # via django-radius -python-cinderclient==2.0.1 # via python-openstackclient, shade -python-dateutil==2.6.0 # via adal, azure-storage +python-cinderclient==2.2.0 # via python-openstackclient, shade +python-dateutil==2.6.0 # via adal, azure-storage, botocore python-designateclient==2.6.0 # via shade -python-glanceclient==2.6.0 # via python-openstackclient -python-ironicclient==1.12.0 # via shade -python-keystoneclient==3.10.0 # via python-neutronclient, python-openstackclient, shade -python-ldap==2.4.38 # via django-auth-ldap +python-glanceclient==2.7.0 # via python-openstackclient +python-ironicclient==1.13.0 # via shade +python-keystoneclient==3.11.0 # via python-neutronclient, python-openstackclient, shade +python-ldap==2.4.39 # via django-auth-ldap python-logstash==0.4.6 python-memcached==1.58 -python-neutronclient==6.2.0 # via shade -python-novaclient==8.0.0 # via python-openstackclient, shade +python-neutronclient==6.3.0 # via shade +python-novaclient==9.0.1 # via python-openstackclient, shade python-openid==2.2.5 # via python-social-auth python-openstackclient==3.11.0 # via python-ironicclient python-radius==1.0 @@ -150,35 +153,36 @@ python-saml==2.2.1 python-social-auth==0.2.21 pytz==2017.2 # via babel, celery, irc, oslo.serialization, oslo.utils, tempora, twilio pyvmomi==6.5 -pyyaml==3.12 # via cliff, djangorestframework-yaml, os-client-config, psphere, python-ironicclient +pyyaml==3.12 # via cliff, djangorestframework-yaml, os-client-config, oslo.config, psphere, python-ironicclient redbaron==0.6.3 requests-futures==0.9.7 requests-oauthlib==0.8.0 # via msrest, python-social-auth -requests==2.11.1 +requests==2.14.2 # via adal, apache-libcloud, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-social-auth, pyvmomi, requests-futures, requests-oauthlib, slackclient, twilio requestsexceptions==1.2.0 # via os-client-config, shade -rfc3986==0.4.1 # via oslo.config +rfc3986==1.0.0 # via oslo.config rply==0.7.4 # via baron +s3transfer==0.1.10 # via boto3 secretstorage==2.3.1 # via keyring service-identity==16.0.0 shade==1.20.0 -simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient +simplejson==3.11.1 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient six==1.10.0 # via asgi-amqp, asgiref, autobahn, cliff, cmd2, cryptography, debtcollector, django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, keystoneauth1, more-itertools, munch, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, packaging, pygerduty, pyopenssl, pyrad, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-memcached, python-neutronclient, python-novaclient, python-openstackclient, python-social-auth, pyvmomi, setuptools, shade, slackclient, stevedore, tacacs-plus, tempora, twilio, txaio, warlock, websocket-client slackclient==1.0.5 -stevedore==1.21.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient +stevedore==1.23.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient suds==0.4 # via psphere tacacs_plus==0.2 -tempora==1.6.1 # via irc, jaraco.logging +tempora==1.7 # via irc, jaraco.logging twilio==6.1.0 twisted==16.6.0 -txaio==2.7.1 # via autobahn +txaio==2.8.0 # via autobahn typing==3.6.1 # via m2crypto unicodecsv==0.14.1 # via cliff uwsgi==2.0.14 warlock==1.2.0 # via python-glanceclient -websocket-client==0.40.0 # via slackclient +websocket-client==0.43.0 # via slackclient wrapt==1.10.10 # via debtcollector, positional, python-glanceclient xmltodict==0.11.0 -zope.interface==4.4.0 # via twisted +zope.interface==4.4.2 # via twisted # The following packages are considered to be unsafe in a requirements file: pip==9.0.1 diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index 7da53931b5..cb6e87e047 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -3,12 +3,12 @@ azure==2.0.0rc6 backports.ssl-match-hostname==3.5.0.1 kombu==3.0.37 boto==2.46.1 +boto3==1.4.4 python-memcached==1.58 psphere==0.5.2 psutil==5.2.2 pyvmomi==6.5 pywinrm[kerberos]==0.2.2 -requests==2.11.1 secretstorage==2.3.1 shade==1.20.0 setuptools==35.0.2 diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index fad2d7ebfe..3fb7751d66 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -30,49 +30,54 @@ azure-storage==0.33.0 # via azure azure==2.0.0rc6 babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-neutronclient, python-novaclient, python-openstackclient backports.ssl-match-hostname==3.5.0.1 +boto3==1.4.4 boto==2.46.1 +botocore==1.5.72 # via boto3, s3transfer certifi==2017.4.17 # via msrest cffi==1.10.0 # via cryptography -cliff==2.6.0 # via osc-lib, python-designateclient, python-neutronclient, python-openstackclient -cmd2==0.7.0 # via cliff -cryptography==1.8.1 # via adal, azure-storage, secretstorage -debtcollector==1.13.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient +cliff==2.7.0 # via osc-lib, python-designateclient, python-neutronclient, python-openstackclient +cmd2==0.7.2 # via cliff +cryptography==1.9 # via adal, azure-storage, pyopenssl, secretstorage +debtcollector==1.15.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient decorator==4.0.11 # via shade deprecation==1.0.1 # via openstacksdk -dogpile.cache==0.6.2 # via python-ironicclient, shade +#docutils==0.12 # via botocore +dogpile.cache==0.6.3 # via python-ironicclient, shade enum34==1.1.6 # via cryptography, msrest funcsigs==1.0.2 # via debtcollector, oslo.utils functools32==3.2.3.post2 # via jsonschema -futures==3.1.1 # via azure-storage, shade +futures==3.1.1 # via azure-storage, s3transfer, shade idna==2.5 # via cryptography ipaddress==1.0.18 # via cryptography, shade iso8601==0.1.11 # via keystoneauth1, oslo.utils, python-neutronclient, python-novaclient isodate==0.5.4 # via msrest -jmespath==0.9.2 # via shade -jsonpatch==1.15 # via shade, warlock +jmespath==0.9.3 # via boto3, botocore, shade +jsonpatch==1.16 # via openstacksdk, shade, warlock jsonpointer==1.10 # via jsonpatch jsonschema==2.6.0 # via python-designateclient, python-ironicclient, warlock -keyring==10.3.2 # via msrestazure -keystoneauth1==2.20.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, shade +keyring==10.3.3 # via msrestazure +keystoneauth1==2.21.0 # via openstacksdk, os-client-config, osc-lib, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, shade kombu==3.0.37 +lxml==3.8.0 # via pyvmomi monotonic==1.3 # via oslo.utils msgpack-python==0.4.8 # via oslo.serialization -msrest==0.4.7 # via azure-common, msrestazure -msrestazure==0.4.7 # via azure-common +msrest==0.4.10 # via azure-common, msrestazure +msrestazure==0.4.9 # via azure-common munch==2.1.1 # via shade netaddr==0.7.19 # via oslo.config, oslo.utils, python-neutronclient -netifaces==0.10.5 # via oslo.utils, shade -ntlm-auth==1.0.3 # via requests-ntlm +netifaces==0.10.6 # via oslo.utils, shade +ntlm-auth==1.0.4 # via requests-ntlm oauthlib==2.0.2 # via requests-oauthlib -openstacksdk==0.9.16 # via python-openstackclient +openstacksdk==0.9.17 # via python-openstackclient +ordereddict==1.1 # via ntlm-auth os-client-config==1.27.0 # via openstacksdk, osc-lib, python-neutronclient, shade osc-lib==1.6.0 # via python-designateclient, python-ironicclient, python-neutronclient, python-openstackclient -oslo.config==4.1.0 # via python-keystoneclient -oslo.i18n==3.15.0 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient +oslo.config==4.6.0 # via python-keystoneclient +oslo.i18n==3.15.3 # via osc-lib, oslo.config, oslo.utils, python-cinderclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient oslo.serialization==2.18.0 # via python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient -oslo.utils==3.25.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient -packaging==16.8 # via cryptography, setuptools -pbr==3.0.0 # via cliff, debtcollector, keystoneauth1, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, requestsexceptions, shade, stevedore +oslo.utils==3.26.0 # via osc-lib, oslo.serialization, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient +packaging==16.8 # via setuptools +pbr==3.1.1 # via cliff, debtcollector, keystoneauth1, openstacksdk, osc-lib, oslo.i18n, oslo.serialization, oslo.utils, positional, python-cinderclient, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, requestsexceptions, shade, stevedore positional==1.1.1 # via keystoneauth1, python-keystoneclient prettytable==0.7.2 # via cliff, python-cinderclient, python-glanceclient, python-ironicclient, python-novaclient psphere==0.5.2 @@ -80,32 +85,34 @@ psutil==5.2.2 pycparser==2.17 # via cffi pyjwt==1.5.0 # via adal pykerberos==1.1.14 # via requests-kerberos +pyopenssl==17.0.0 # via pyvmomi pyparsing==2.2.0 # via cliff, cmd2, oslo.utils, packaging -python-cinderclient==2.0.1 # via python-openstackclient, shade -python-dateutil==2.6.0 # via adal, azure-storage +python-cinderclient==2.2.0 # via python-openstackclient, shade +python-dateutil==2.6.0 # via adal, azure-storage, botocore python-designateclient==2.6.0 # via shade -python-glanceclient==2.6.0 # via python-openstackclient -python-ironicclient==1.12.0 # via shade -python-keystoneclient==3.10.0 # via python-neutronclient, python-openstackclient, shade +python-glanceclient==2.7.0 # via python-openstackclient +python-ironicclient==1.13.0 # via shade +python-keystoneclient==3.11.0 # via python-neutronclient, python-openstackclient, shade python-memcached==1.58 -python-neutronclient==6.2.0 # via shade -python-novaclient==8.0.0 # via python-openstackclient, shade +python-neutronclient==6.3.0 # via shade +python-novaclient==9.0.1 # via python-openstackclient, shade python-openstackclient==3.11.0 # via python-ironicclient pytz==2017.2 # via babel, oslo.serialization, oslo.utils pyvmomi==6.5 pywinrm[kerberos]==0.2.2 -pyyaml==3.12 # via cliff, os-client-config, psphere, python-ironicclient +pyyaml==3.12 # via cliff, os-client-config, oslo.config, psphere, python-ironicclient requests-kerberos==0.11.0 # via pywinrm requests-ntlm==1.0.0 # via pywinrm requests-oauthlib==0.8.0 # via msrest -requests==2.11.1 +requests==2.14.2 # via adal, apache-libcloud, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-neutronclient, pyvmomi, pywinrm, requests-kerberos, requests-ntlm, requests-oauthlib requestsexceptions==1.2.0 # via os-client-config, shade -rfc3986==0.4.1 # via oslo.config +rfc3986==1.0.0 # via oslo.config +s3transfer==0.1.10 # via boto3 secretstorage==2.3.1 shade==1.20.0 -simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient -six==1.10.0 # via cliff, cmd2, cryptography, debtcollector, keystoneauth1, munch, ntlm-auth, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, packaging, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-memcached, python-neutronclient, python-novaclient, python-openstackclient, pyvmomi, pywinrm, setuptools, shade, stevedore, warlock -stevedore==1.21.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient +simplejson==3.11.1 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient +six==1.10.0 # via cliff, cmd2, cryptography, debtcollector, keystoneauth1, munch, ntlm-auth, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, packaging, pyopenssl, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-memcached, python-neutronclient, python-novaclient, python-openstackclient, pyvmomi, pywinrm, setuptools, shade, stevedore, warlock +stevedore==1.23.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient suds==0.4 # via psphere unicodecsv==0.14.1 # via cliff warlock==1.2.0 # via python-glanceclient diff --git a/requirements/requirements_ansible_git.txt b/requirements/requirements_ansible_git.txt index e69de29bb2..425e2e171f 100644 --- a/requirements/requirements_ansible_git.txt +++ b/requirements/requirements_ansible_git.txt @@ -0,0 +1 @@ +git+https://github.com/ansible/docutils.git@master#egg=docutils diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index 3b65bbc4b3..7ccc5a7227 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -2,3 +2,4 @@ git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield +git+https://github.com/ansible/docutils.git@master#egg=docutils From d1ec8f93c0939aba2a4b70bfb8d26716aca78472 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 29 Jun 2017 16:30:43 -0400 Subject: [PATCH 06/69] Support deploying Tower into OpenShift --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4c004017e7..1d4552496d 100644 --- a/Makefile +++ b/Makefile @@ -933,7 +933,8 @@ install: $(PYTHON) setup.py install $(SETUP_INSTALL_ARGS) docker-auth: - docker login -e 1234@5678.com -u oauth2accesstoken -p "$(GCLOUD_AUTH)" https://gcr.io + #docker login -e "1234@5678.com" -u oauth2accesstoken -p "$(GCLOUD_AUTH)" https://gcr.io + docker login -u oauth2accesstoken -p "$(GCLOUD_AUTH)" https://gcr.io # Docker Compose Development environment docker-compose: docker-auth From 413e8c3bc935c514c9b49f8825275c7ae2e10c77 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 29 Jun 2017 16:55:11 -0400 Subject: [PATCH 07/69] isolated nodes should report their awx version in their heartbeat see: #6810 --- awx/main/isolated/isolated_manager.py | 3 ++- awx/main/isolated/run.py | 8 ++++++++ awx/plugins/isolated/tower_capacity.py | 10 +++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/awx/main/isolated/isolated_manager.py b/awx/main/isolated/isolated_manager.py index 0f2a6b6837..9a5e940a58 100644 --- a/awx/main/isolated/isolated_manager.py +++ b/awx/main/isolated/isolated_manager.py @@ -374,8 +374,9 @@ class IsolatedManager(object): logger.exception('Failed to read status from isolated instance {}.'.format(instance.hostname)) continue if 'capacity' in task_result: + instance.version = task_result['version'] instance.capacity = int(task_result['capacity']) - instance.save(update_fields=['capacity', 'modified']) + instance.save(update_fields=['capacity', 'version', 'modified']) else: logger.warning('Could not update capacity of {}, msg={}'.format( instance.hostname, task_result.get('msg', 'unknown failure'))) diff --git a/awx/main/isolated/run.py b/awx/main/isolated/run.py index 39387f9ee6..0b466228aa 100755 --- a/awx/main/isolated/run.py +++ b/awx/main/isolated/run.py @@ -259,10 +259,18 @@ def __run__(private_data_dir): if __name__ == '__main__': + __version__ = 'devel' + try: + import awx + __version__ = awx.__version__ + except ImportError: + pass # in devel, `awx` isn't an installed package parser = argparse.ArgumentParser(description='manage a daemonized, isolated ansible playbook') + parser.add_argument('--version', action='version', version=__version__ + '-isolated') parser.add_argument('command', choices=['start', 'stop', 'is-alive']) parser.add_argument('private_data_dir') args = parser.parse_args() + private_data_dir = args.private_data_dir pidfile = os.path.join(private_data_dir, 'pid') diff --git a/awx/plugins/isolated/tower_capacity.py b/awx/plugins/isolated/tower_capacity.py index 03bbb0cecd..9ef879b423 100644 --- a/awx/plugins/isolated/tower_capacity.py +++ b/awx/plugins/isolated/tower_capacity.py @@ -24,6 +24,14 @@ def main(): module = AnsibleModule( argument_spec = dict() ) + try: + version = subprocess.check_output( + ['tower-expect', '--version'], + stderr=subprocess.STDOUT + ).strip() + except subprocess.CalledProcessError as e: + module.fail_json(msg=str(e)) + return # Duplicated with awx.main.utils.common.get_system_task_capacity try: out = subprocess.check_output(['free', '-m']) @@ -36,7 +44,7 @@ def main(): cap = 50 + ((int(total_mem_value) / 1024) - 2) * 75 # Module never results in a change - module.exit_json(changed=False, capacity=cap) + module.exit_json(changed=False, capacity=cap, version=version) if __name__ == '__main__': From f40182fef8588e84df8c674b8740fa39ab1d46ea Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 29 Jun 2017 16:34:02 -0400 Subject: [PATCH 08/69] Apply accumulated validation for CTinT all endpoint. --- awx/conf/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/conf/serializers.py b/awx/conf/serializers.py index b6abe6d961..0f588d7ee7 100644 --- a/awx/conf/serializers.py +++ b/awx/conf/serializers.py @@ -70,6 +70,10 @@ class SettingSingletonSerializer(serializers.Serializer): category_slug = self.context['view'].kwargs.get('category_slug', 'all') except (KeyError, AttributeError): category_slug = '' + if self.context['view'].kwargs.get('category_slug', '') == 'all': + for validate_func in settings_registry._validate_registry.values(): + attrs = validate_func(self, attrs) + return attrs custom_validate = settings_registry.get_registered_validate_func(category_slug) return custom_validate(self, attrs) if custom_validate else attrs From 727568c80d8b0b41131cdab54763e61ec3486e24 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 29 Jun 2017 17:33:19 -0400 Subject: [PATCH 09/69] Add translation for components and credentials * Add typeahead feature of the Lookup component --- .../credentials/add-credentials.controller.js | 27 +++++----- .../add-edit-credentials.view.html | 12 ++--- .../credentials/credentials.strings.js | 34 +++++++++++++ .../edit-credentials.controller.js | 24 +++++---- awx/ui/client/features/credentials/index.js | 51 +++++++++++++------ .../credentials/legacy.credentials.js | 4 +- .../lib/components/components.strings.js | 49 ++++++++++++++++++ .../lib/components/form/action.directive.js | 8 +-- .../lib/components/form/form.directive.js | 14 +++-- awx/ui/client/lib/components/index.js | 4 +- .../lib/components/input/base.controller.js | 38 ++++++++------ .../components/input/checkbox.directive.js | 2 +- .../lib/components/input/group.directive.js | 13 +++-- .../lib/components/input/group.partial.html | 2 +- .../lib/components/input/label.partial.html | 2 +- .../lib/components/input/lookup.directive.js | 35 +++++++++++-- .../lib/components/input/lookup.partial.html | 4 +- .../lib/components/input/secret.directive.js | 10 ++-- .../lib/components/input/select.directive.js | 8 ++- .../input/textarea-secret.directive.js | 16 +++--- .../lib/components/modal/modal.partial.html | 2 +- .../lib/components/tabs/group.directive.js | 10 +--- awx/ui/client/lib/models/Base.js | 25 ++++++++- awx/ui/client/lib/models/CredentialType.js | 7 +++ .../lib/services/base-string.service.js | 23 +++++++++ awx/ui/client/lib/services/index.js | 4 +- awx/ui/client/src/i18n.js | 8 ++- awx/ui/grunt-tasks/nggettext_extract.js | 16 ++++-- 28 files changed, 335 insertions(+), 117 deletions(-) create mode 100644 awx/ui/client/features/credentials/credentials.strings.js create mode 100644 awx/ui/client/lib/components/components.strings.js create mode 100644 awx/ui/client/lib/services/base-string.service.js diff --git a/awx/ui/client/features/credentials/add-credentials.controller.js b/awx/ui/client/features/credentials/add-credentials.controller.js index 88ed9b01b0..c94efd2ee1 100644 --- a/awx/ui/client/features/credentials/add-credentials.controller.js +++ b/awx/ui/client/features/credentials/add-credentials.controller.js @@ -1,21 +1,19 @@ -const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION'; - -function AddCredentialsController (models, $state) { +function AddCredentialsController (models, $state, strings) { let vm = this || {}; let me = models.me; let credential = models.credential; let credentialType = models.credentialType; + let organization = models.organization; - vm.panelTitle = 'NEW CREDENTIAL'; + vm.mode = 'add'; + vm.strings = strings.credentials; + + vm.panelTitle = vm.strings[vm.mode].PANEL_TITLE; vm.tab = { - details: { - _active: true - }, - permissions:{ - _disabled: true - } + details: { _active: true }, + permissions:{ _disabled: true } }; vm.form = credential.createFormSchema('post', { @@ -24,9 +22,13 @@ function AddCredentialsController (models, $state) { vm.form.organization._resource = 'organization'; vm.form.organization._route = 'credentials.add.organization'; - + vm.form.organization._model = organization; + vm.form.organization._placeholder = vm.strings.inputs.ORGANIZATION_PLACEHOLDER; + vm.form.credential_type._resource = 'credential_type'; vm.form.credential_type._route = 'credentials.add.credentialType'; + vm.form.credential_type._model = credentialType; + vm.form.credential_type._placeholder = vm.strings.inputs.CREDENTIAL_TYPE_PLACEHOLDER; vm.form.inputs = { _get: id => { @@ -52,7 +54,8 @@ function AddCredentialsController (models, $state) { AddCredentialsController.$inject = [ 'resolvedModels', - '$state' + '$state', + 'CredentialsStrings' ]; export default AddCredentialsController; diff --git a/awx/ui/client/features/credentials/add-edit-credentials.view.html b/awx/ui/client/features/credentials/add-edit-credentials.view.html index a49b5dbed0..5625be6dd1 100644 --- a/awx/ui/client/features/credentials/add-edit-credentials.view.html +++ b/awx/ui/client/features/credentials/add-edit-credentials.view.html @@ -2,8 +2,8 @@ {{ vm.panelTitle }} - Details - Permissions + {{ vm.strings.tab.DETAILS }} + {{ vm.strings.tab.PERMISSIONS }} @@ -17,7 +17,7 @@ - Type Details + {{ vm.strings.inputs.GROUP_TITLE }} @@ -29,11 +29,11 @@ - CREDENTIALS PERMISSIONS + {{ vm.strings.permissions.TITLE }} - Details - Permissions + {{ vm.strings.tab.DETAILS }} + {{ vm.strings.tab.PERMISSIONS }} diff --git a/awx/ui/client/features/credentials/credentials.strings.js b/awx/ui/client/features/credentials/credentials.strings.js new file mode 100644 index 0000000000..16cd98a840 --- /dev/null +++ b/awx/ui/client/features/credentials/credentials.strings.js @@ -0,0 +1,34 @@ +function CredentialsStrings (BaseString) { + BaseString.call(this, 'credentials'); + + let t = this.t; + let ns = this.credentials; + + ns.state = { + ADD_BREADCRUMB_LABEL: t('CREATE CREDENTIAL'), + EDIT_BREADCRUMB_LABEL: t('EDIT CREDENTIAL') + }; + + ns.tab = { + DETAILS: t('Details'), + PERMISSIONS: t('Permissions') + }; + + ns.inputs = { + GROUP_TITLE: t('Type Details'), + ORGANIZATION_PLACEHOLDER: t('SELECT AN ORGANIZATION'), + CREDENTIAL_TYPE_PLACEHOLDER: t('SELECT A CREDENTIAL TYPE') + }; + + ns.add = { + PANEL_TITLE: t('NEW CREDENTIAL') + }; + + ns.permissions = { + TITLE: t('CREDENTIALS PERMISSIONS') + }; +} + +CredentialsStrings.$inject = ['BaseStringService']; + +export default CredentialsStrings; diff --git a/awx/ui/client/features/credentials/edit-credentials.controller.js b/awx/ui/client/features/credentials/edit-credentials.controller.js index 69126a5988..5412e6f544 100644 --- a/awx/ui/client/features/credentials/edit-credentials.controller.js +++ b/awx/ui/client/features/credentials/edit-credentials.controller.js @@ -1,12 +1,15 @@ -const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION'; - -function EditCredentialsController (models, $state, $scope) { +function EditCredentialsController (models, $state, $scope, strings) { let vm = this || {}; let me = models.me; let credential = models.credential; let credentialType = models.credentialType; - let selectedCredentialType = credentialType.getById(credential.get('credential_type')); + let organization = models.organization; + let selectedCredentialType = models.selectedCredentialType; + + vm.mode = 'edit'; + vm.strings = strings.credentials; + vm.panelTitle = credential.get('name'); vm.tab = { details: { @@ -33,21 +36,23 @@ function EditCredentialsController (models, $state, $scope) { // Only exists for permissions compatibility $scope.credential_obj = credential.get(); - vm.panelTitle = credential.get('name'); - vm.form = credential.createFormSchema('put', { omit: ['user', 'team', 'inputs'] }); vm.form.organization._resource = 'organization'; + vm.form.organization._model = organization; vm.form.organization._route = 'credentials.edit.organization'; vm.form.organization._value = credential.get('summary_fields.organization.id'); vm.form.organization._displayValue = credential.get('summary_fields.organization.name'); + vm.form.organization._placeholder = vm.strings.inputs.ORGANIZATION_PLACEHOLDER; vm.form.credential_type._resource = 'credential_type'; + vm.form.credential_type._model = credentialType; vm.form.credential_type._route = 'credentials.edit.credentialType'; - vm.form.credential_type._value = selectedCredentialType.id; - vm.form.credential_type._displayValue = selectedCredentialType.name; + vm.form.credential_type._value = selectedCredentialType.get('id'); + vm.form.credential_type._displayValue = selectedCredentialType.get('name'); + vm.form.credential_type._placeholder = vm.strings.inputs.CREDENTIAL_TYPE_PLACEHOLDER; vm.form.inputs = { _get (id) { @@ -80,7 +85,8 @@ function EditCredentialsController (models, $state, $scope) { EditCredentialsController.$inject = [ 'resolvedModels', '$state', - '$scope' + '$scope', + 'CredentialsStrings' ]; export default EditCredentialsController; diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index 0eb1e02233..f203ff471f 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -1,23 +1,38 @@ import LegacyCredentials from './legacy.credentials'; -import AddController from './add-credentials.controller.js'; -import EditController from './edit-credentials.controller.js'; -import { N_ } from '../../src/i18n'; +import AddController from './add-credentials.controller'; +import EditController from './edit-credentials.controller'; +import CredentialsStrings from './credentials.strings' -function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType) { +function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) { let id = $stateParams.credential_id; + let models; let promises = { me: new Me('get'), - credentialType: new CredentialType('get') + credentialType: new CredentialType('get'), + organization: new Organization('get') }; - if (id) { - promises.credential = new Credential(['get', 'options'], [id, id]); - } else { + if (!id) { promises.credential = new Credential('options'); + + return $q.all(promises) } - return $q.all(promises); + promises.credential = new Credential(['get', 'options'], [id, id]); + + return $q.all(promises) + .then(_models_ => { + models = _models_; + let credentialTypeId = models.credential.get('credential_type'); + + return models.credentialType.graft(credentialTypeId); + }) + .then(selectedCredentialType => { + models.selectedCredentialType = selectedCredentialType; + + return models; + }); } CredentialsResolve.$inject = [ @@ -25,19 +40,23 @@ CredentialsResolve.$inject = [ '$stateParams', 'MeModel', 'CredentialModel', - 'CredentialTypeModel' + 'CredentialTypeModel', + 'OrganizationModel' ]; -function CredentialsConfig ($stateExtenderProvider, legacyProvider, pathProvider) { +function CredentialsConfig ($stateExtenderProvider, legacyProvider, pathProvider, stringProvider) { let path = pathProvider.$get(); let stateExtender = $stateExtenderProvider.$get(); let legacy = legacyProvider.$get(); + let strings = stringProvider.$get(); + + strings = strings.credentials.state; stateExtender.addState({ name: 'credentials.add', route: '/add', ncyBreadcrumb: { - label: N_('CREATE CREDENTIALS') + label: strings.ADD_BREADCRUMB_LABEL }, views: { 'add@credentials': { @@ -55,7 +74,7 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, pathProvider name: 'credentials.edit', route: '/:credential_id', ncyBreadcrumb: { - label: N_('EDIT') + label: strings.EDIT_BREADCRUMB_LABEL }, views: { 'edit@credentials': { @@ -81,7 +100,8 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, pathProvider CredentialsConfig.$inject = [ '$stateExtenderProvider', 'LegacyCredentialsServiceProvider', - 'PathServiceProvider' + 'PathServiceProvider', + 'CredentialsStringsProvider' ]; angular @@ -89,4 +109,5 @@ angular .config(CredentialsConfig) .controller('AddController', AddController) .controller('EditController', EditController) - .service('LegacyCredentialsService', LegacyCredentials); + .service('LegacyCredentialsService', LegacyCredentials) + .service('CredentialsStrings', CredentialsStrings); diff --git a/awx/ui/client/features/credentials/legacy.credentials.js b/awx/ui/client/features/credentials/legacy.credentials.js index c0a55d559c..95cda7e94a 100644 --- a/awx/ui/client/features/credentials/legacy.credentials.js +++ b/awx/ui/client/features/credentials/legacy.credentials.js @@ -113,7 +113,7 @@ function LegacyCredentialsService (pathService) { }, ncyBreadcrumb: { parent: 'credentials.edit', - label: 'PERMISSIONS' + label: N_('PERMISSIONS') }, views: { 'related': { @@ -336,7 +336,7 @@ function LegacyCredentialsService (pathService) { return this.credentialType; default: - throw new Error(`Legacy state configuration for ${name} does not exist`); + throw new Error(N_(`Legacy state configuration for ${name} does not exist`)); }; }; } diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js new file mode 100644 index 0000000000..804dfd81ce --- /dev/null +++ b/awx/ui/client/lib/components/components.strings.js @@ -0,0 +1,49 @@ +function ComponentsStrings (BaseString) { + BaseString.call(this, 'components'); + + let t = this.t; + let ns = this.components; + + ns.REPLACE = t('REPLACE'); + ns.REVERT = t('REVERT'); + ns.ENCRYPTED = t('ENCRYPTED'); + ns.OPTIONS = t('OPTIONS'); + ns.SHOW = t('SHOW'); + ns.HIDE = t('HIDE'); + + ns.message = { + REQUIRED_INPUT_MISSING: t('Please enter a value.'), + INVALID_INPUT: t('Invalid input for this type.') + }; + + ns.form = { + SUBMISSION_ERROR_TITLE: t('Unable to Submit'), + SUBMISSION_ERROR_MESSAGE:t('Unexpected server error. View the console for more information'), + SUBMISSION_ERROR_PREFACE: t('Unexpected Error') + }; + + ns.group = { + UNSUPPORTED_ERROR_PREFACE: t('Unsupported input type') + }; + + ns.label = { + PROMPT_ON_LAUNCH: t('Prompt on launch') + }; + + ns.select = { + UNSUPPORTED_TYPE_ERROR: t('Unsupported display model type'), + EMPTY_PLACEHOLDER: t('NO OPTIONS AVAILABLE') + }; + + ns.textarea = { + SSH_KEY_HINT: t('HINT: Drag and drop an SSH private key file on the field below.') + }; + + ns.lookup = { + NOT_FOUND: t('That value was not found. Please enter or select a valid value.') + }; +} + +ComponentsStrings.$inject = ['BaseStringService']; + +export default ComponentsStrings; diff --git a/awx/ui/client/lib/components/form/action.directive.js b/awx/ui/client/lib/components/form/action.directive.js index 1cc9c4ded2..33db4182c7 100644 --- a/awx/ui/client/lib/components/form/action.directive.js +++ b/awx/ui/client/lib/components/form/action.directive.js @@ -5,7 +5,7 @@ function link (scope, element, attrs, controllers) { actionController.init(formController, element, scope); } -function atFormActionController ($state) { +function atFormActionController ($state, strings) { let vm = this || {}; let element; @@ -36,21 +36,21 @@ function atFormActionController ($state) { }; vm.setCancelDefaults = () => { - scope.text = 'CANCEL'; + scope.text = strings.CANCEL; scope.fill = 'Hollow'; scope.color = 'default'; scope.action = () => $state.go(scope.to || '^'); }; vm.setSaveDefaults = () => { - scope.text = 'SAVE'; + scope.text = strings.SAVE; scope.fill = ''; scope.color = 'success'; scope.action = () => form.submit(); }; } -atFormAction.$inject = ['$state']; +atFormActionController.$inject = ['$state', 'ComponentsStrings']; function atFormAction (pathService) { return { diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js index d85ca856fd..75d3aa596a 100644 --- a/awx/ui/client/lib/components/form/form.directive.js +++ b/awx/ui/client/lib/components/form/form.directive.js @@ -8,13 +8,15 @@ function atFormLink (scope, el, attrs, controllers) { formController.init(scope, form); } -function AtFormController (eventService) { +function AtFormController (eventService, strings) { let vm = this || {}; let scope; let modal; let form; + strings = strings.components.forms; + vm.components = []; vm.state = { isValid: false, @@ -99,6 +101,8 @@ function AtFormController (eventService) { if (!handled) { let message; + let title = strings.SUBMISSION_ERROR_TITLE; + let preface = strings.SUBMISSION_ERROR_PREFACE; if (typeof err.data === 'object') { message = JSON.stringify(err.data); @@ -106,13 +110,13 @@ function AtFormController (eventService) { message = err.data; } - modal.show('Unable to Submit', `Unexpected Error: ${message}`); + modal.show(title, `${preface}: ${message}`) } }; vm.handleUnexpectedError = err => { - let title = 'Unable to Submit'; - let message = 'Unexpected server error. View the console for more information'; + let title = strings.SUBMISSION_ERROR_TITLE; + let message = strings.SUBMISSION_ERROR_MESSAGE; modal.show(title, message); @@ -190,7 +194,7 @@ function AtFormController (eventService) { }; } -AtFormController.$inject = ['EventService']; +AtFormController.$inject = ['EventService', 'ComponentsStrings']; function atForm (pathService) { return { diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index e9a79c8d4c..67b425204a 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -20,6 +20,7 @@ import tab from './tabs/tab.directive'; import tabGroup from './tabs/group.directive'; import BaseInputController from './input/base.controller'; +import ComponentsStrings from './components.strings'; angular .module('at.lib.components', []) @@ -43,6 +44,7 @@ angular .directive('atPopover', popover) .directive('atTab', tab) .directive('atTabGroup', tabGroup) - .service('BaseInputController', BaseInputController); + .service('BaseInputController', BaseInputController) + .service('ComponentsStrings', ComponentsStrings); diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 71591ca129..1b27b159cd 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -1,17 +1,19 @@ -const REQUIRED_INPUT_MISSING_MESSAGE = 'Please enter a value.'; -const DEFAULT_INVALID_INPUT_MESSAGE = 'Invalid input for this type.'; -const PROMPT_ON_LAUNCH_VALUE = 'ASK'; -const ENCRYPTED_VALUE = '$encrypted$'; +function BaseInputController (strings) { + // Default values are universal. Don't translate. + const PROMPT_ON_LAUNCH_VALUE = 'ASK'; + const ENCRYPTED_VALUE = '$encrypted$'; -function BaseInputController () { return function extend (type, scope, element, form) { let vm = this; + vm.strings = strings; + scope.state = scope.state || {}; + scope.state._touched = false; scope.state._required = scope.state.required || false; - scope.state._isValid = scope.state.isValid || false; - scope.state._disabled = scope.state.disabled || false; + scope.state._isValid = scope.state._isValid || false; + scope.state._disabled = scope.state._disabled || false; scope.state._activeModel = '_value'; if (scope.state.ask_at_runtime) { @@ -43,17 +45,19 @@ function BaseInputController () { let isValid = true; let message = ''; - if (scope.state._required && !scope.state._value) { - isValid = false; - message = REQUIRED_INPUT_MISSING_MESSAGE; + if (scope.state._value || scope.state._displayValue) { + scope.state._touched = true; } - if (scope.state.validate) { + if (scope.state._required && !scope.state._value && !scope.state._displayValue) { + isValid = false; + message = vm.strings.components.message.REQUIRED_INPUT_MISSING; + } else if (scope.state._validate) { let result = scope.state._validate(scope.state._value); if (!result.isValid) { isValid = false; - message = result.message || DEFAULT_INVALID_INPUT_MESSAGE; + message = result.message || vm.strings.components.message.INVALID_INPUT; } } @@ -66,7 +70,7 @@ function BaseInputController () { vm.check = () => { let result = vm.validate(); - if (result.isValid !== scope.state._isValid) { + if (scope.state._touched || !scope.state._required) { scope.state._rejected = !result.isValid; scope.state._isValid = result.isValid; scope.state._message = result.message; @@ -79,14 +83,14 @@ function BaseInputController () { scope.state._isBeingReplaced = !scope.state._isBeingReplaced; if (!scope.state._isBeingReplaced) { - scope.state._buttonText = 'REPLACE'; + scope.state._buttonText = vm.strings.components.REPLACE; scope.state._disabled = true; scope.state._enableToggle = true; scope.state._value = scope.state._preEditValue; scope.state._activeModel = '_displayValue'; - scope.state._placeholder = 'ENCRYPTED'; + scope.state._placeholder = vm.strings.components.ENCRYPTED; } else { - scope.state._buttonText = 'REVERT'; + scope.state._buttonText = vm.strings.components.REVERT; scope.state._disabled = false; scope.state._enableToggle = false; scope.state._activeModel = '_value'; @@ -118,4 +122,6 @@ function BaseInputController () { }; } +BaseInputController.$inject = ['ComponentsStrings']; + export default BaseInputController; diff --git a/awx/ui/client/lib/components/input/checkbox.directive.js b/awx/ui/client/lib/components/input/checkbox.directive.js index 9380eae846..99728cb744 100644 --- a/awx/ui/client/lib/components/input/checkbox.directive.js +++ b/awx/ui/client/lib/components/input/checkbox.directive.js @@ -15,7 +15,7 @@ function AtInputCheckboxController (baseInputController) { vm.init = (scope, element, form) => { baseInputController.call(vm, 'input', scope, element, form); scope.label = scope.state.label; - scope.state.label = 'OPTIONS'; + scope.state.label = vm.strings.components.OPTIONS; vm.check(); }; diff --git a/awx/ui/client/lib/components/input/group.directive.js b/awx/ui/client/lib/components/input/group.directive.js index 550cd1c0ef..865a54611a 100644 --- a/awx/ui/client/lib/components/input/group.directive.js +++ b/awx/ui/client/lib/components/input/group.directive.js @@ -34,14 +34,14 @@ function AtInputGroupController ($scope, $compile) { }; vm.update = () => { - if (!vm.isValidSource()) { - return; - } - if (state._group) { vm.clear(); } + if (!vm.isValidSource()) { + return; + } + state._value = source._value; let inputs = state._get(source._value); @@ -101,7 +101,8 @@ function AtInputGroupController ($scope, $compile) { config._data = input.choices; config._exp = 'index as choice for (index, choice) in state._data'; } else { - throw new Error('Unsupported input type: ' + input.type) + let preface = vm.strings.components.UNSUPPORTED_ERROR_PREFACE; + throw new Error(`${preface}: ${input.type}`) } return config; @@ -158,6 +159,8 @@ function AtInputGroupController ($scope, $compile) { vm.clear = () => { form.deregisterInputGroup(state._group); element.innerHTML = ''; + state._group = undefined; + state._value = undefined; }; } diff --git a/awx/ui/client/lib/components/input/group.partial.html b/awx/ui/client/lib/components/input/group.partial.html index 6d20836d6a..45d4a845e0 100644 --- a/awx/ui/client/lib/components/input/group.partial.html +++ b/awx/ui/client/lib/components/input/group.partial.html @@ -1,4 +1,4 @@ -
+
diff --git a/awx/ui/client/lib/components/input/label.partial.html b/awx/ui/client/lib/components/input/label.partial.html index d53a4a7a25..2a9a61690f 100644 --- a/awx/ui/client/lib/components/input/label.partial.html +++ b/awx/ui/client/lib/components/input/label.partial.html @@ -8,7 +8,7 @@ -

Prompt on launch

+

{{ vm.strings.components.label.PROMPT_ON_LAUNCH }}

diff --git a/awx/ui/client/lib/components/input/lookup.directive.js b/awx/ui/client/lib/components/input/lookup.directive.js index 50e7ddef88..f8845ddd19 100644 --- a/awx/ui/client/lib/components/input/lookup.directive.js +++ b/awx/ui/client/lib/components/input/lookup.directive.js @@ -20,12 +20,13 @@ function AtInputLookupController (baseInputController, $state, $stateParams) { scope = _scope_; scope.$watch(scope.state._resource, vm.watchResource); + scope.state._validate = vm.checkOnInput; vm.check(); }; vm.watchResource = () => { - if (scope[scope.state._resource]) { + if (scope[scope.state._resource] !== scope.state._value) { scope.state._value = scope[scope.state._resource]; scope.state._displayValue = scope[`${scope.state._resource}_name`]; @@ -33,15 +34,43 @@ function AtInputLookupController (baseInputController, $state, $stateParams) { } }; - vm.search = () => { + vm.lookup = () => { let params = {}; - if (scope.state._value) { + if (scope.state._value && scope.state._isValid) { params.selected = scope.state._value; } $state.go(scope.state._route, params); }; + + vm.reset = () => { + scope.state._value = undefined; + scope[scope.state._resource] = undefined; + }; + + vm.checkOnInput = () => { + if (!scope.state._touched) { + return { isValid: true }; + } + + let result = scope.state._model.match('get', 'name', scope.state._displayValue); + + if (result) { + scope[scope.state._resource] = result.id; + scope.state._value = result.id; + scope.state._displayValue = result.name; + + return { isValid: true }; + } + + vm.reset(); + + return { + isValid: false, + message: vm.strings.components.lookup.NOT_FOUND + }; + }; } AtInputLookupController.$inject = [ diff --git a/awx/ui/client/lib/components/input/lookup.partial.html b/awx/ui/client/lib/components/input/lookup.partial.html index 6f4d7a0a68..39900afcc1 100644 --- a/awx/ui/client/lib/components/input/lookup.partial.html +++ b/awx/ui/client/lib/components/input/lookup.partial.html @@ -6,13 +6,13 @@ { if (scope.type === 'password') { scope.type = 'text'; - scope.state._buttonText = 'HIDE'; + scope.state._buttonText = vm.strings.components.HIDE; } else { scope.type = 'password'; - scope.state._buttonText = 'SHOW'; + scope.state._buttonText = vm.strings.components.SHOW; } }; } diff --git a/awx/ui/client/lib/components/input/select.directive.js b/awx/ui/client/lib/components/input/select.directive.js index cbc6c2b0fa..d61c13f33d 100644 --- a/awx/ui/client/lib/components/input/select.directive.js +++ b/awx/ui/client/lib/components/input/select.directive.js @@ -1,5 +1,3 @@ -const DEFAULT_EMPTY_PLACEHOLDER = 'NO OPTIONS AVAILABLE'; - function atInputSelectLink (scope, element, attrs, controllers) { let formController = controllers[0]; let inputController = controllers[1]; @@ -11,7 +9,7 @@ function atInputSelectLink (scope, element, attrs, controllers) { inputController.init(scope, element, formController); } -function AtInputSelectController (baseInputController, eventService) { +function AtInputSelectController (baseInputController, eventService) { let vm = this || {}; let scope; @@ -29,7 +27,7 @@ function AtInputSelectController (baseInputController, eventService) { if (!scope.state._data || scope.state._data.length === 0) { scope.state._disabled = true; - scope.state._placeholder = DEFAULT_EMPTY_PLACEHOLDER; + scope.state._placeholder = vm.strings.components.EMPTY_PLACEHOLDER; } vm.setListeners(); @@ -67,7 +65,7 @@ function AtInputSelectController (baseInputController, eventService) { } else if (scope.state._format === 'grouped-object') { scope.displayModel = scope.state._value[scope.state._display]; } else { - throw new Error('Unsupported display model type'); + throw new Error(vm.strings.components.UNSUPPORTED_TYPE_ERROR); } }; } diff --git a/awx/ui/client/lib/components/input/textarea-secret.directive.js b/awx/ui/client/lib/components/input/textarea-secret.directive.js index a22491b3de..ffe15c2741 100644 --- a/awx/ui/client/lib/components/input/textarea-secret.directive.js +++ b/awx/ui/client/lib/components/input/textarea-secret.directive.js @@ -1,5 +1,3 @@ -const DEFAULT_HINT = 'HINT: Drag and drop an SSH private key file on the field below.'; - function atInputTextareaSecretLink (scope, element, attrs, controllers) { let formController = controllers[0]; let inputController = controllers[1]; @@ -28,13 +26,13 @@ function AtInputTextareaSecretController (baseInputController, eventService) { if (scope.state.format === 'ssh_private_key') { scope.ssh = true; - scope.state._hint = scope.state._hint || DEFAULT_HINT; + scope.state._hint = scope.state._hint || vm.strings.components.textarea.SSH_KEY_HINT; input = element.find('input')[0]; } if (scope.state._value) { - scope.state._buttonText = 'REPLACE'; - scope.state._placeholder = 'ENCRYPTED'; + scope.state._buttonText = vm.strings.components.REPLACE; + scope.state._placeholder = vm.strings.components.ENCRYPTED; } else { if (scope.state.format === 'ssh_private_key') { vm.listeners = vm.setFileListeners(textarea, input); @@ -54,7 +52,7 @@ function AtInputTextareaSecretController (baseInputController, eventService) { vm.listeners = vm.setFileListeners(textarea, input); } else { scope.state._displayHint = false; - scope.state._placeholder = 'ENCRYPTED'; + scope.state._placeholder = vm.strings.components.ENCRYPTED; eventService.remove(vm.listeners); } }; @@ -92,7 +90,11 @@ function AtInputTextareaSecretController (baseInputController, eventService) { }; } -AtInputTextareaSecretController.$inject = ['BaseInputController', 'EventService']; +AtInputTextareaSecretController.$inject = [ + 'BaseInputController', + 'EventService', + 'ComponentsStrings' +]; function atInputTextareaSecret (pathService) { return { diff --git a/awx/ui/client/lib/components/modal/modal.partial.html b/awx/ui/client/lib/components/modal/modal.partial.html index 66db4d9d49..4ff2f44a99 100644 --- a/awx/ui/client/lib/components/modal/modal.partial.html +++ b/awx/ui/client/lib/components/modal/modal.partial.html @@ -23,7 +23,7 @@
diff --git a/awx/ui/client/lib/components/tabs/group.directive.js b/awx/ui/client/lib/components/tabs/group.directive.js index a78da714cf..907d8853fa 100644 --- a/awx/ui/client/lib/components/tabs/group.directive.js +++ b/awx/ui/client/lib/components/tabs/group.directive.js @@ -18,16 +18,8 @@ function AtTabGroupController ($state) { }; vm.register = tab => { - tab.active = true; -/* - * if (vm.tabs.length === 0) { - * tab.active = true; - * } else { - * tab.disabled = true; - * } - * - */ + vm.tabs.push(tab); }; } diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 8f4e04cbc2..d1c8fd2936 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -12,8 +12,10 @@ function request (method, resource) { } function httpGet (resource) { + this.method = this.method || 'GET'; + let req = { - method: 'GET', + method: this.method, url: this.path }; @@ -85,6 +87,26 @@ function get (keys) { return this.find('get', keys); } +function match (method, key, value) { + let model = this.model[method.toUpperCase()]; + + if (!model) { + return null; + } + + if (!model.results) { + if (model[key] === value) { + return model; + } + + return null; + } + + let result = model.results.filter(result => result[key] === value); + + return result.length === 0 ? null : result[0]; +} + function find (method, keys) { let value = this.model[method.toUpperCase()]; @@ -138,6 +160,7 @@ function BaseModel (path) { this.get = get; this.options = options; this.find = find; + this.match = match; this.normalizePath = normalizePath; this.getById = getById; this.request = request; diff --git a/awx/ui/client/lib/models/CredentialType.js b/awx/ui/client/lib/models/CredentialType.js index 2679e191d7..f0ab0d6f0f 100644 --- a/awx/ui/client/lib/models/CredentialType.js +++ b/awx/ui/client/lib/models/CredentialType.js @@ -26,11 +26,18 @@ function mergeInputProperties (type) { }); } +function graft (id) { + let data = this.getById(id); + + return new CredentialTypeModel('get', data); +} + function CredentialTypeModel (method, id) { BaseModel.call(this, 'credential_types'); this.categorizeByKind = categorizeByKind.bind(this); this.mergeInputProperties = mergeInputProperties.bind(this); + this.graft = graft.bind(this); return this.request(method, id) .then(() => this); diff --git a/awx/ui/client/lib/services/base-string.service.js b/awx/ui/client/lib/services/base-string.service.js new file mode 100644 index 0000000000..7226a921a9 --- /dev/null +++ b/awx/ui/client/lib/services/base-string.service.js @@ -0,0 +1,23 @@ +let i18n; + +function BaseStringService (namespace) { + let t = i18n._; + + this.t = t; + this[namespace] = {}; + + this.CANCEL = t('CANCEL'); + this.SAVE = t('SAVE'); + this.OK = t('OK'); +} + + +function BaseStringServiceLoader (_i18n_) { + i18n = _i18n_; + + return BaseStringService; +} + +BaseStringServiceLoader.$inject = ['i18n']; + +export default BaseStringServiceLoader; diff --git a/awx/ui/client/lib/services/index.js b/awx/ui/client/lib/services/index.js index d6a256b0fc..f82bc49616 100644 --- a/awx/ui/client/lib/services/index.js +++ b/awx/ui/client/lib/services/index.js @@ -1,7 +1,9 @@ import EventService from './event.service'; import PathService from './path.service'; +import BaseStringService from './base-string.service'; angular .module('at.lib.services', []) .service('EventService', EventService) - .service('PathService', PathService); + .service('PathService', PathService) + .service('BaseStringService', BaseStringService); diff --git a/awx/ui/client/src/i18n.js b/awx/ui/client/src/i18n.js index 2b24a09822..062c2455ea 100644 --- a/awx/ui/client/src/i18n.js +++ b/awx/ui/client/src/i18n.js @@ -1,6 +1,7 @@ /* jshint ignore:start */ var sprintf = require('sprintf-js').sprintf; +let defaultLanguage = 'en_US'; /** * @ngdoc method @@ -24,7 +25,12 @@ export default $window.navigator.userLanguage || ''; var langUrl = langInfo.replace('-', '_'); - //gettextCatalog.debug = true; + + if (langUrl === defaultLanguage) { + return; + } + + // gettextCatalog.debug = true; gettextCatalog.setCurrentLanguage(langInfo); gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json'); }; diff --git a/awx/ui/grunt-tasks/nggettext_extract.js b/awx/ui/grunt-tasks/nggettext_extract.js index de297658e3..b264b6ada8 100644 --- a/awx/ui/grunt-tasks/nggettext_extract.js +++ b/awx/ui/grunt-tasks/nggettext_extract.js @@ -1,11 +1,19 @@ +let source = [ + 'client/features/**/*.js', + 'client/features/**/*.html', + 'client/lib/**/*.js', + 'client/lib/**/*.html', + 'client/src/**/*.js', + 'client/src/**/*.html' +]; + module.exports = { all: { options: { - markerNames: ['_', 'N_'] + markerNames: ['_', 'N_', 't'] }, files: { - 'po/ansible-tower-ui.pot': ['client/src/**/*.js', - 'client/src/**/*.html'] + 'po/ansible-tower-ui.pot': source } - }, + } }; From 32ddd8dab9cfa1ade8f34f1a7299d142929ed818 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 29 Jun 2017 16:00:35 -0700 Subject: [PATCH 10/69] changes to insights - better error handling for an invventory missing an insights cred - better error handling for missing reports/no reports - feedback from UX - adding refresh button --- .../inventories/insights/insights.block.less | 23 ++++++++++++++++--- .../insights/insights.controller.js | 11 +++++---- .../insights/insights.partial.html | 14 ++++++++++- .../edit/inventory-edit.controller.js | 1 + .../standard-inventory/inventory.form.js | 2 +- .../src/shared/branding/colors.default.less | 1 + 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.block.less b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.block.less index f651c30cf3..e1ac5a80f7 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.block.less +++ b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.block.less @@ -64,14 +64,15 @@ } .InsightsNav-anchor.is-currentFilter{ + background-color: @f2grey; padding-top: 5px; - border-bottom: 5px solid @menu-link-btm-hov; + border-bottom: 5px solid @b7grey; } .InsightsNav-anchor:hover{ - background-color: @menu-link-bg-hov; + background-color: @f2grey; padding-top: 5px; - border-bottom: 5px solid @menu-link-btm-hov; + border-bottom: 5px solid @b7grey; } .InsightsNav-totalIssues{ @@ -99,6 +100,22 @@ background-color: @b7grey; } +.InsightsNav-refresh{ + color: @default-icon; + cursor: pointer; + margin-left: 10px; + margin-right: 10px; +} + +.InsightsNav-refresh:hover{ + color: @default-link; +} + +.InsightsBody-missingIssues{ + color: @default-icon; + margin: 10px 0px 10px 0px; +} + .InsightsRow{ margin-top:10px; } diff --git a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.controller.js b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.controller.js index b050866277..88c70ee00a 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.controller.js @@ -9,8 +9,8 @@ export default [ 'InsightsData', '$scope', 'moment', '$state', 'InventoryData', function (data, $scope, moment, $state, InventoryData, InsightsService) { function init() { - $scope.reports = data.reports; - $scope.reports_dataset = data; + $scope.reports = (data && data.reports) ? data.reports : []; + $scope.reports_dataset = (data) ? data : {}; $scope.currentFilter = "total"; $scope.solvable_count = filter('solvable').length; $scope.not_solvable_count = filter('not_solvable').length; @@ -20,8 +20,11 @@ function (data, $scope, moment, $state, InventoryData, InsightsService) { $scope.low_count =filter('low').length; let a = moment(), b = moment($scope.reports_dataset.last_check_in); $scope.last_check_in = a.diff(b, 'hours'); - $scope.inventory = InventoryData; - $scope.insights_credential = InventoryData.summary_fields.insights_credential.id; + $scope.inventory = (InventoryData) ? InventoryData : {}; + $scope.insights_credential = (InventoryData && InventoryData.summary_fields && + InventoryData.summary_fields.insights_credential && InventoryData.summary_fields.insights_credential.id) ? + InventoryData.summary_fields.insights_credential.id : null; + } function filter(str){ diff --git a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.partial.html b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.partial.html index e1f2ee91d3..fa05459114 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/insights/insights.partial.html +++ b/awx/ui/client/src/inventories-hosts/inventories/insights/insights.partial.html @@ -50,10 +50,22 @@
Not Solvable With Playbook
{{not_solvable_count}}
+
+ + +
+
+ No scan jobs have been run on this host. +
+
+ The Insights Credential for {{inventory.name}} was not found. +
- +
diff --git a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/edit/inventory-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/edit/inventory-edit.controller.js index 443a65be35..a32c5ab49e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/edit/inventory-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/edit/inventory-edit.controller.js @@ -32,6 +32,7 @@ function InventoriesEdit($scope, $location, $scope.insights_credential_name = (inventoryData.summary_fields.insights_credential && inventoryData.summary_fields.insights_credential.name) ? inventoryData.summary_fields.insights_credential.name : null; $scope.insights_credential = (inventoryData.summary_fields.insights_credential && inventoryData.summary_fields.insights_credential.id) ? inventoryData.summary_fields.insights_credential.id : null; + $scope.is_insights = (inventoryData.summary_fields.insights_credential && inventoryData.summary_fields.insights_credential.id) ? true : false; $scope.organization_name = inventoryData.summary_fields.organization.name; $scope.inventory_variables = inventoryData.variables === null || inventoryData.variables === '' ? '---' : ParseVariableString(inventoryData.variables); $scope.parseType = 'yaml'; diff --git a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js index 5d4c24d811..0be972390c 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js @@ -201,7 +201,7 @@ function(i18n, InventoryCompletedJobsList) { relatedButtons: { remediate_inventory: { ngClick: 'remediateInventory(id, name, insights_credential)', - ngShow: 'insights_credential!==null && mode !== "add"', + ngShow: 'is_insights && mode !== "add"', label: i18n._('Remediate Inventory'), class: 'Form-primaryButton' } diff --git a/awx/ui/client/src/shared/branding/colors.default.less b/awx/ui/client/src/shared/branding/colors.default.less index 1f7ca0b2d0..a685f8427e 100644 --- a/awx/ui/client/src/shared/branding/colors.default.less +++ b/awx/ui/client/src/shared/branding/colors.default.less @@ -24,6 +24,7 @@ @cgrey: #CCCCCC; @f7grey: #F7F7F7; @insights-yellow: #dedc4f; +@f2grey: #f2f2f2; @default-warning: #F0AD4E; From 668bce8212f5b45360a31f9cbd1928db16a35283 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 29 Jun 2017 13:27:17 -0400 Subject: [PATCH 11/69] fix job launch deadlock * This both fixes the deadlock problem and a logic problem. We shouldn't set the job's job_template current_job to pending jobs. --- awx/main/models/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 0142dd957a..b7b5ca8073 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -712,7 +712,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique result = super(UnifiedJob, self).save(*args, **kwargs) # If status changed, update the parent instance. - if self.status != status_before: + if self.status != status_before and self.status != 'pending': self._update_parent_instance() # Done. From de84f3cf4a7cc0a077091f84da6385827873fa3e Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 30 Jun 2017 11:28:04 -0400 Subject: [PATCH 12/69] fix a bug in cluster node version comparison --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f4dd816042..68172a3085 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -189,7 +189,7 @@ def cluster_node_heartbeat(self): for other_inst in recent_inst: if other_inst.version == "": continue - if Version(other_inst.version) > Version(tower_application_version): + if Version(other_inst.version.split('-', 1)[0]) > Version(tower_application_version): logger.error("Host {} reports Tower version {}, but this node {} is at {}, shutting down".format(other_inst.hostname, other_inst.version, inst.hostname, From 40e749c8f73e5cb60e8b26e061fa6c035a5fe79e Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Fri, 30 Jun 2017 12:14:44 -0400 Subject: [PATCH 13/69] Show Isolated Badge in Job Details See #6560 --- .../src/job-results/job-results.block.less | 18 ++++++++++++++++++ .../src/job-results/job-results.partial.html | 6 +++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 5ed1de0b7b..63be335c97 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -112,6 +112,24 @@ font-family: monospace; } +.JobResults-resultRowText--instanceGroup { + display: flex; +} + +.JobResults-isolatedBadge { + align-items: center; + align-self: center; + background-color: @default-list-header-bg; + border-radius: 5px; + color: @default-stdout-txt; + display: flex; + font-size: 10px; + height: 16px; + margin-left: 10px; + padding: 0 10px; + text-transform: uppercase; +} + .JobResults-statusResultIcon { padding-left: 0px; padding-right: 10px; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index ee39e70cc0..23799aace3 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -363,8 +363,12 @@ -
+
{{ job.summary_fields.instance_group.name }} + + Isolated +
From dade5c12a73856601f055ebc755b8b8ba7d9f30e Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 30 Jun 2017 12:23:44 -0400 Subject: [PATCH 14/69] fix a bug in the CredentialType field validator that breaks `required` see: #6769 --- awx/main/fields.py | 4 ++++ .../functional/api/test_credential_type.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/awx/main/fields.py b/awx/main/fields.py index 325ae25967..e523afa16d 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -542,6 +542,10 @@ class CredentialTypeInputField(JSONSchemaField): 'type': 'object', 'additionalProperties': False, 'properties': { + 'required': { + 'type': 'array', + 'items': {'type': 'string'} + }, 'fields': { 'type': 'array', 'items': { diff --git a/awx/main/tests/functional/api/test_credential_type.py b/awx/main/tests/functional/api/test_credential_type.py index e0666e60fa..eea1d42e8a 100644 --- a/awx/main/tests/functional/api/test_credential_type.py +++ b/awx/main/tests/functional/api/test_credential_type.py @@ -170,6 +170,30 @@ def test_create_with_valid_inputs(get, post, admin): assert fields[0]['type'] == 'string' +@pytest.mark.django_db +def test_create_with_required_inputs(get, post, admin): + response = post(reverse('api:credential_type_list'), { + 'kind': 'cloud', + 'name': 'MyCloud', + 'inputs': { + 'fields': [{ + 'id': 'api_token', + 'label': 'API Token', + 'type': 'string', + 'secret': True + }], + 'required': ['api_token'], + }, + 'injectors': {} + }, admin) + assert response.status_code == 201 + + response = get(reverse('api:credential_type_list'), admin) + assert response.data['count'] == 1 + required = response.data['results'][0]['inputs']['required'] + assert required == ['api_token'] + + @pytest.mark.django_db @pytest.mark.parametrize('inputs', [ True, From 29adb39caec5da1473f05ae4a7d66d8d873bfb87 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 30 Jun 2017 13:52:41 -0400 Subject: [PATCH 15/69] add insights_cred_id to host->inventory summary fields --- awx/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bd481f20e3..8b39357843 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -76,7 +76,8 @@ SUMMARIZABLE_FK_FIELDS = { 'total_inventory_sources', 'inventory_sources_with_failures', 'organization_id', - 'kind'), + 'kind', + 'insights_credential_id',), 'host': DEFAULT_SUMMARY_FIELDS + ('has_active_failures', 'has_inventory_sources'), 'group': DEFAULT_SUMMARY_FIELDS + ('has_active_failures', From 46f5f5da000eb832974e6cd669a8360a84f16837 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 21 Jun 2017 11:43:56 -0400 Subject: [PATCH 16/69] Implementation of Instance Groups read-only views in Tower Settings --- awx/ui/client/legacy-styles/lists.less | 23 +++++- .../src/bread-crumb/bread-crumb.block.less | 7 ++ .../src/bread-crumb/bread-crumb.partial.html | 2 +- .../capacity-bar/capacity-bar.block.less | 28 +++++++ .../capacity-bar/capacity-bar.directive.js | 18 ++++ .../capacity-bar/capacity-bar.partial.html | 4 + .../src/instance-groups/capacity-bar/main.js | 5 ++ .../instance-groups/instance-group.block.less | 56 +++++++++++++ .../instance-group.partial.html | 34 ++++++++ .../instance-groups.block.less | 4 - .../instance-groups/instance-groups.list.js | 4 +- .../instance-groups.partial.html | 11 +++ .../instance-groups/instance-groups.route.js | 30 +++++++ .../instance-jobs/instance-jobs-list.route.js | 31 +++++++ .../instance-jobs/instance-jobs.controller.js | 82 +++++++++++++++++++ .../instance-jobs/instance-jobs.list.js | 78 ++++++++++++++++++ .../instance-jobs/instance-jobs.partial.html | 33 ++++++++ .../instance-jobs/instance-jobs.route.js | 37 +++++++++ .../instances/instances-list.partial.html | 43 ++++++++++ .../instances/instances-list.route.js | 34 ++++++++ .../instances/instances.controller.js | 20 +++++ .../instances/instances.list.js | 29 +++++++ .../instances/instances.route.js | 34 ++++++++ .../instance-groups/jobs/jobs-list.route.js | 39 +++++++++ .../instance-groups/jobs/jobs.controller.js | 82 +++++++++++++++++++ .../src/instance-groups/jobs/jobs.list.js | 76 +++++++++++++++++ .../list/instance-groups-list.controller.js | 37 ++------- .../list/instance-groups-list.partial.html | 63 ++++++++++++++ awx/ui/client/src/instance-groups/main.js | 63 +++++++++----- awx/ui/client/src/shared/generator-helpers.js | 2 + .../instance-groups-modal.directive.js | 2 +- .../instance-groups-modal.partial.html | 10 +-- .../instance-groups-multiselect.controller.js | 0 .../instance-groups.block.less | 22 +++++ .../instance-groups.directive.js | 0 .../instance-groups.partial.html | 6 +- 36 files changed, 982 insertions(+), 67 deletions(-) create mode 100644 awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less create mode 100644 awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js create mode 100644 awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html create mode 100644 awx/ui/client/src/instance-groups/capacity-bar/main.js create mode 100644 awx/ui/client/src/instance-groups/instance-group.block.less create mode 100644 awx/ui/client/src/instance-groups/instance-group.partial.html delete mode 100644 awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less create mode 100644 awx/ui/client/src/instance-groups/instance-groups.partial.html create mode 100644 awx/ui/client/src/instance-groups/instance-groups.route.js create mode 100644 awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js create mode 100644 awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js create mode 100644 awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js create mode 100644 awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html create mode 100644 awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js create mode 100644 awx/ui/client/src/instance-groups/instances/instances-list.partial.html create mode 100644 awx/ui/client/src/instance-groups/instances/instances-list.route.js create mode 100644 awx/ui/client/src/instance-groups/instances/instances.controller.js create mode 100644 awx/ui/client/src/instance-groups/instances/instances.list.js create mode 100644 awx/ui/client/src/instance-groups/instances/instances.route.js create mode 100644 awx/ui/client/src/instance-groups/jobs/jobs-list.route.js create mode 100644 awx/ui/client/src/instance-groups/jobs/jobs.controller.js create mode 100644 awx/ui/client/src/instance-groups/jobs/jobs.list.js create mode 100644 awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html rename awx/ui/client/src/{instance-groups => shared}/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js (97%) rename awx/ui/client/src/{instance-groups => shared}/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.partial.html (77%) rename awx/ui/client/src/{instance-groups => shared}/instance-groups-multiselect/instance-groups-multiselect.controller.js (100%) create mode 100644 awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less rename awx/ui/client/src/{instance-groups => shared}/instance-groups-multiselect/instance-groups.directive.js (100%) rename awx/ui/client/src/{instance-groups => shared}/instance-groups-multiselect/instance-groups.partial.html (68%) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 484ea4440c..6aae815332 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -60,7 +60,7 @@ table, tbody { height: 40px; font-size: 14px; color: @list-item; - border-bottom: 1px solid @default-white-button-bord; + border-bottom: 1px solid @default-border; } .List-tableRow:last-of-type { @@ -176,6 +176,27 @@ table, tbody { text-transform: uppercase; } +.List-exitHolder { + justify-content: flex-end; + display:flex; +} + +.List-exit { + cursor:pointer; + padding:0px; + border: none; + height:20px; + font-size: 20px; + background-color:@default-bg; + color:@d7grey; + transition: color 0.2s; + line-height:1; +} + +.List-exit:hover{ + color:@default-icon; +} + .List-actionHolder { justify-content: flex-end; display: flex; diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.block.less b/awx/ui/client/src/bread-crumb/bread-crumb.block.less index 7f080e7b8b..f75894afeb 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.block.less +++ b/awx/ui/client/src/bread-crumb/bread-crumb.block.less @@ -34,6 +34,13 @@ .BreadCrumb-menuLink:hover { color: @bc-link-icon-focus; } +.BreadCrumb-menuLink { + .BreadCrumb-menuLinkImage.fa-refresh { + &:hover { + color: @default-link; + } + } +} .BreadCrumb-menuLinkImage { font-size: 18px; color: @bc-link-icon; diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html index 563e6c4f79..a95c4d2996 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html +++ b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html @@ -8,7 +8,7 @@ data-trigger="hover" data-container="body" ng-hide= "loadingLicense || licenseMissing" - ng-if="socketStatus === 'error' && showRefreshButton" + ng-if="(socketStatus === 'error' && showRefreshButton) || $state.includes('instanceGroups')" ng-click="refresh()"> diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less new file mode 100644 index 0000000000..f8e9af45ee --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less @@ -0,0 +1,28 @@ +@import "../../shared/branding/colors.default.less"; + +capacity-bar { + + width: 50%; + margin-right: 10px; + min-width: 100px; + + .CapacityBar { + background-color: @default-bg; + display: flex; + flex: 0 0 auto; + height: 10px; + border: 1px solid @default-link; + width: 100%; + border-radius: 100vw; + overflow: hidden; + } + + .CapacityBar-remaining { + background-color: @default-link; + flex: 0 0 auto; + } + + .CapacityBar-consumed { + flex: 0 0 auto; + } +} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js new file mode 100644 index 0000000000..37fd496349 --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js @@ -0,0 +1,18 @@ +export default ['templateUrl', + function (templateUrl) { + return { + scope: { + capacity: '=' + }, + templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'), + restrict: 'E', + link: function(scope) { + scope.$watch('capacity', function() { + scope.PercentRemainingStyle = { + 'flex-grow': scope.capacity * 0.01 + }; + }, true); + } + }; + } +]; diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html new file mode 100644 index 0000000000..aac4ccd7b0 --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/capacity-bar/main.js b/awx/ui/client/src/instance-groups/capacity-bar/main.js new file mode 100644 index 0000000000..e330c7080c --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/main.js @@ -0,0 +1,5 @@ +import capacityBar from './capacity-bar.directive'; + +export default + angular.module('capacityBarDirective', []) + .directive('capacityBar', capacityBar); \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-group.block.less b/awx/ui/client/src/instance-groups/instance-group.block.less new file mode 100644 index 0000000000..8588e0f2de --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-group.block.less @@ -0,0 +1,56 @@ +@import "../shared/branding/colors.default.less"; + +.InstanceGroups { + + .BreadCrumb-menuLinkImage:hover { + color: @default-link; + } + + .List-details { + align-self: flex-end; + color: @default-interface-txt; + display: flex; + flex: 0 0 auto; + font-size: 12px; + margin-right:20px; + text-transform: uppercase; + } + + .Capacity-details { + display: flex; + margin-right: 20px; + align-items: center; + + .Capacity-details--label { + color: @default-interface-txt; + margin: 0 10px 0 0; + } + + .Capacity-details--percentage { + color: @default-data-txt; + } + } + + .RunningJobs-details { + align-items: center; + display: flex; + + .RunningJobs-details--label { + margin: 0 10px 0 0; + } + } + + .List-tableCell--capacityRemainingColumn { + display: flex; + height: 40px; + align-items: center; + } + + .List-noItems { + margin-top: 20px; + } + + .List-tableRow .List-titleBadge { + margin: 0 0 0 5px; + } +} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-group.partial.html b/awx/ui/client/src/instance-groups/instance-group.partial.html new file mode 100644 index 0000000000..cb8724a85b --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-group.partial.html @@ -0,0 +1,34 @@ +
+
+
+
+
+
{{ instanceGroupName | translate }}
+
+
+
+

Capacity

+ + {{ instanceGroupCapacity | translate }}% +
+
+

Running Jobs

+ + {{ instanceGroupJobsRunning | translate}} + +
+
+
+ +
+
+
+
INSTANCES
+
JOBS
+
+
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less b/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less deleted file mode 100644 index a3aab73a19..0000000000 --- a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less +++ /dev/null @@ -1,4 +0,0 @@ -#InstanceGroups { - display: flex; - padding: 0 12px; -} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-groups.list.js b/awx/ui/client/src/instance-groups/instance-groups.list.js index a6827a9291..096d2652bf 100644 --- a/awx/ui/client/src/instance-groups/instance-groups.list.js +++ b/awx/ui/client/src/instance-groups/instance-groups.list.js @@ -14,8 +14,10 @@ export default ['i18n', function(i18n) { label: i18n._('Name'), columnClass: 'col-md-3 col-sm-9 col-xs-9', modalColumnClass: 'col-md-8', + uiSref: 'instanceGroups.instances.list({instance_group_id: instance_group.id})', + ngClass: "{'isActive' : isActive()}" }, - capacity: { + percent_capacity_remaining: { label: i18n._('Capacity'), nosort: true, }, diff --git a/awx/ui/client/src/instance-groups/instance-groups.partial.html b/awx/ui/client/src/instance-groups/instance-groups.partial.html new file mode 100644 index 0000000000..baeaf59f00 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-groups.partial.html @@ -0,0 +1,11 @@ +
+ + +
+ +
+ +
+
+
+
diff --git a/awx/ui/client/src/instance-groups/instance-groups.route.js b/awx/ui/client/src/instance-groups/instance-groups.route.js new file mode 100644 index 0000000000..b9ed461fa3 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-groups.route.js @@ -0,0 +1,30 @@ +import {templateUrl} from '../shared/template-url/template-url.factory'; +import { N_ } from '../i18n'; + +export default { + name: 'instanceGroups', + url: '/instance_groups', + searchPrefix: 'instance_group', + ncyBreadcrumb: { + parent: 'setup', + label: N_('INSTANCE GROUPS') + }, + views: { + '@': { + templateUrl: templateUrl('./instance-groups/instance-groups'), + }, + 'list@instanceGroups': { + templateUrl: templateUrl('./instance-groups/list/instance-groups-list'), + controller: 'InstanceGroupsList' + + } + }, + resolve: { + Dataset: ['InstanceGroupList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js new file mode 100644 index 0000000000..cc66f4168d --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js @@ -0,0 +1,31 @@ +import { N_ } from '../../../i18n'; + +export default { + name: 'instanceGroups.instances.list.job.list', + url: '/jobs', + searchPrefix: 'instance_job', + ncyBreadcrumb: { + parent: 'instanceGroups.instances.list', + label: N_('{{ breadcrumb.instance_name }}') + }, + views: { + 'list@instanceGroups.instances.list.job': { + templateProvider: function(InstanceJobsList, generateList) { + let html = generateList.build({ + list: InstanceJobsList + }); + return html; + }, + controller: 'InstanceJobsController' + } + }, + + resolve: { + Dataset: ['InstanceJobsList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instances')}${$stateParams.instance_id}/jobs`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js new file mode 100644 index 0000000000..a7d50764f5 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js @@ -0,0 +1,82 @@ +export default ['$scope','InstanceJobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, InstanceJobsList, GetBasePath, Rest, Dataset, Find, $state, $q) { + + let list = InstanceJobsList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + + if($scope[list.name] && $scope[list.name].length > 0) { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } + + // Set the item type label + if (list.fields.type && $scope.options && + $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.forEach(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + } + }); + } + buildTooltips(itm); + }); + } + } + + function buildTooltips(job) { + job.status_tip = 'Job ' + job.status + ". Click for details."; + } + + $scope.viewjobResults = function(job) { + var goTojobResults = function(state) { + $state.go(state, { id: job.id }, { reload: true }); + }; + switch (job.type) { + case 'job': + goTojobResults('jobResult'); + break; + case 'ad_hoc_command': + goTojobResults('adHocJobStdout'); + break; + case 'system_job': + goTojobResults('managementJobStdout'); + break; + case 'project_update': + goTojobResults('scmUpdateStdout'); + break; + case 'inventory_update': + goTojobResults('inventorySyncStdout'); + break; + case 'workflow_job': + goTojobResults('workflowResults'); + break; + } + }; + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js new file mode 100644 index 0000000000..58476f0054 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js @@ -0,0 +1,78 @@ +export default ['i18n', function(i18n) { + return { + + name: 'instance_jobs', + iterator: 'instance_job', + index: false, + hover: false, + well: false, + emptyListText: i18n._('No jobs have yet run.'), + title: false, + basePath: 'api/v2/instances/{{$stateParams.instance_id}}/jobs', + + fields: { + status: { + label: '', + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', + dataTipWatch: 'instance_job.status_tip', + awToolTip: "{{ instance_job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ instance_job.status_popover_title }}", + icon: 'icon-job-{{ instance_job.status }}', + iconOnly: true, + ngClick:"viewjobResults(instance_job)", + nosort: true + }, + id: { + label: i18n._('ID'), + ngClick:"viewjobResults(instance_job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ instance_job.status_tip }}", + dataPlacement: 'top', + noLink: true + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', + ngClick: "viewjobResults(instance_job)", + nosort: true, + badgePlacement: 'right', + badgeCustom: true, + badgeIcon: ` + + W + + ` + }, + type: { + label: i18n._('Type'), + ngBind: 'instance_job.type_label', + link: false, + columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs", + nosort: true + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true, + nosort: true + }, + labels: { + label: i18n._('Labels'), + type: 'labels', + nosort: true, + showDelete: false, + columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs', + sourceModel: 'labels', + sourceField: 'name', + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html new file mode 100644 index 0000000000..20bf3e0fcd --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html @@ -0,0 +1,33 @@ +
+
+
+
+
+
{{ instanceName | translate }}
+
+
+
+

Capacity

+ + {{ instanceCapacity | translate }}% +
+
+

Running Jobs

+ + {{ instanceJobsRunning | translate }} + +
+
+
+ +
+
+
+
JOBS
+
+
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js new file mode 100644 index 0000000000..73af9259b5 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js @@ -0,0 +1,37 @@ +import { templateUrl } from '../../../shared/template-url/template-url.factory'; + +export default { + name: 'instanceGroups.instances.list.job', + url: '/:instance_id', + abstract: true, + ncyBreadcrumb: { + skip: true + }, + views: { + 'instanceJobs@instanceGroups': { + templateUrl: templateUrl('./instance-groups/instances/instance-jobs/instance-jobs'), + controller: function($scope, $rootScope, instance) { + $scope.instanceName = instance.hostname; + $scope.instanceCapacity = instance.percent_capacity_remaining; + $scope.instanceJobsRunning = instance.jobs_running; + $rootScope.breadcrumb.instance_name = instance.hostname; + } + } + }, + resolve: { + instance: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) { + let url = GetBasePath('instances') + $stateParams.instance_id; + Rest.setUrl(url); + return Rest.get() + .then(({data}) => { + return data; + }) + .catch(({data, status}) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get instance groups info. GET returned status: ' + status + }); + }); + }] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html new file mode 100644 index 0000000000..9342125735 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html @@ -0,0 +1,43 @@ +
+ + + +
PLEASE ADD ITEMS TO THIS LIST
+
+ + + + + + + + + + + + + + + + +
+ "{{'Name' | translate}}" + + + Capacity + + Running Jobs +
+ {{ instance.hostname | translate }} + {{ instance.percent_capacity_remaining | translate }}% + + + {{ instance.jobs_running | translate }} + +
+
+
diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.route.js b/awx/ui/client/src/instance-groups/instances/instances-list.route.js new file mode 100644 index 0000000000..6b95b33bea --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances-list.route.js @@ -0,0 +1,34 @@ +import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { N_ } from '../../i18n'; + +export default { + name: 'instanceGroups.instances.list', + url: '/instances', + searchPrefix: 'instance', + ncyBreadcrumb: { + parent: 'instanceGroups', + label: N_('{{breadcrumb.instance_group_name}}') + }, + params: { + instance_search: { + value: { + page_size: '5', + order_by: 'hostname' + } + } + }, + views: { + 'list@instanceGroups.instances': { + templateUrl: templateUrl('./instance-groups/instances/instances-list'), + controller: 'InstanceListController' + } + }, + resolve: { + Dataset: ['InstanceList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/instances`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances.controller.js b/awx/ui/client/src/instance-groups/instances/instances.controller.js new file mode 100644 index 0000000000..0481d84263 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.controller.js @@ -0,0 +1,20 @@ +export default ['$scope', 'InstanceList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, InstanceList, GetBasePath, Rest, Dataset, Find, $state, $q) { + let list = InstanceList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.isActive = function(id) { + let selected = parseInt($state.params.instance_id); + return id === selected; + }; + + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances.list.js b/awx/ui/client/src/instance-groups/instances/instances.list.js new file mode 100644 index 0000000000..b3966884d8 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.list.js @@ -0,0 +1,29 @@ +export default ['i18n', function(i18n) { + return { + name: 'instances' , + iterator: 'instance', + listTitle: false, + index: false, + hover: false, + tabs: true, + well: true, + + fields: { + hostname: { + key: true, + label: i18n._('Name'), + columnClass: 'col-md-3 col-sm-9 col-xs-9', + modalColumnClass: 'col-md-8', + uiSref: 'instanceGroups.instances.list.job({instance_id: instance.id})' + }, + percent_capacity_remaining: { + label: i18n._('Capacity'), + nosort: true, + }, + jobs_running: { + label: i18n._('Running Jobs'), + nosort: true, + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/instances/instances.route.js b/awx/ui/client/src/instance-groups/instances/instances.route.js new file mode 100644 index 0000000000..0662052c67 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.route.js @@ -0,0 +1,34 @@ +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'instanceGroups.instances', + url: '/:instance_group_id', + abstract: true, + views: { + 'instances@instanceGroups': { + templateUrl: templateUrl('./instance-groups/instance-group'), + controller: function($scope, $rootScope, instanceGroup) { + $scope.instanceGroupName = instanceGroup.name; + $scope.instanceGroupCapacity = instanceGroup.percent_capacity_remaining; + $scope.instanceGroupJobsRunning = instanceGroup.jobs_running; + $rootScope.breadcrumb.instance_group_name = instanceGroup.name; + } + } + }, + resolve: { + instanceGroup: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) { + let url = GetBasePath('instance_groups') + $stateParams.instance_group_id; + Rest.setUrl(url); + return Rest.get() + .then(({data}) => { + return data; + }) + .catch(({data, status}) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get instance groups info. GET returned status: ' + status + }); + }); + }] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js new file mode 100644 index 0000000000..d21aa8a669 --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js @@ -0,0 +1,39 @@ +import { N_ } from '../../i18n'; + +export default { + name: 'instanceGroups.instances.jobs', + url: '/jobs', + searchPrefix: 'job', + ncyBreadcrumb: { + parent: 'instanceGroups.instances.list', + label: N_('JOBS') + }, + params: { + instance_group_job_search: { + value: { + page_size: '5', + order_by: 'name' + } + }, + instance_group_id: null + }, + views: { + 'list@instanceGroups.instances': { + templateProvider: function(JobsList, generateList) { + let html = generateList.build({ + list: JobsList + }); + return html; + }, + controller: 'JobsListController' + } + }, + resolve: { + Dataset: ['JobsList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/jobs`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.controller.js b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js new file mode 100644 index 0000000000..cfe2f73327 --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js @@ -0,0 +1,82 @@ +export default ['$scope','JobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, JobsList, GetBasePath, Rest, Dataset, Find, $state, $q) { + + let list = JobsList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + + if($scope[list.name] && $scope[list.name].length > 0) { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } + + // Set the item type label + if (list.fields.type && $scope.options && + $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.forEach(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + } + }); + } + buildTooltips(itm); + }); + } + } + + function buildTooltips(job) { + job.status_tip = 'Job ' + job.status + ". Click for details."; + } + + $scope.viewjobResults = function(job) { + var goTojobResults = function(state) { + $state.go(state, { id: job.id }, { reload: true }); + }; + switch (job.type) { + case 'job': + goTojobResults('jobResult'); + break; + case 'ad_hoc_command': + goTojobResults('adHocJobStdout'); + break; + case 'system_job': + goTojobResults('managementJobStdout'); + break; + case 'project_update': + goTojobResults('scmUpdateStdout'); + break; + case 'inventory_update': + goTojobResults('inventorySyncStdout'); + break; + case 'workflow_job': + goTojobResults('workflowResults'); + break; + } + + }; + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.list.js b/awx/ui/client/src/instance-groups/jobs/jobs.list.js new file mode 100644 index 0000000000..59e14ba19b --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs.list.js @@ -0,0 +1,76 @@ +export default ['i18n', function (i18n) { + return { + name: 'jobs', + iterator: 'job', + basePath: 'api/v2/instance_groups/{{$stateParams.instance_group_id}}/jobs/', + index: false, + hover: false, + well: true, + emptyListText: i18n._('No jobs have yet run.'), + listTitle: false, + + fields: { + status: { + label: '', + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', + dataTipWatch: 'job.status_tip', + awToolTip: "{{ job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ job.status_popover_title }}", + icon: 'icon-job-{{ job.status }}', + iconOnly: true, + ngClick: "viewjobResults(job)", + nosort: true + }, + id: { + label: i18n._('ID'), + ngClick: "viewjobResults(job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ job.status_tip }}", + dataPlacement: 'top', + noLink: true + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', + ngClick: "viewjobResults(job)", + badgePlacement: 'right', + badgeCustom: true, + nosort: true, + badgeIcon: ` + + W + + ` + }, + type: { + label: i18n._('Type'), + ngBind: 'job.type_label', + columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs", + nosort: true + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true, + nosort: true + }, + labels: { + label: i18n._('Labels'), + type: 'labels', + nosort: true, + showDelete: false, + columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs', + sourceModel: 'labels', + sourceField: 'name' + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js index ffb46ac5c0..381e2419bf 100644 --- a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js @@ -1,42 +1,19 @@ -export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', - function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state, $q) { +export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', + function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state) { let list = InstanceGroupList; init(); function init(){ - $scope.optionsDefer = $q.defer(); $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.instanceGroupCount = Dataset.data.count; } - // iterate over the list and add fields like type label, after the - // OPTIONS request returns, or the list is sorted/paginated/searched - function optionsRequestDataProcessing(){ - $scope.optionsDefer.promise.then(function(options) { - if($scope.list.name === 'instance_groups'){ - if ($scope[list.name] !== undefined) { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - // Set the item type label - if (list.fields.kind && options && options.actions && options.actions.GET && options.actions.GET.kind) { - options.actions.GET.kind.choices.forEach(function(choice) { - if (choice[0] === item.kind) { - itm.kind_label = choice[1]; - } - }); - } - - }); - } - } - }); - } - - $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); + $scope.isActive = function(id) { + let selected = parseInt($state.params.instance_group_id); + return id === selected; + }; } ]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html new file mode 100644 index 0000000000..6692769205 --- /dev/null +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html @@ -0,0 +1,63 @@ +
+
+
+ INSTANCE GROUPS +
+ + {{ instanceGroupCount | translate}} + +
+
+ + + + +
PLEASE ADD ITEMS TO THIS LIST
+ +
+ + + + + + + + + + + + + + + + +
+ "{{'Name' | translate}}" + + + Capacity + + Running Jobs +
+ {{ instance_group.name | translate }} + {{ instance_group.instances | translate }} + + {{ instance_group.percent_capacity_remaining | translate }}% + + + {{ instance_group.jobs_running | translate }} + +
+
+ + + diff --git a/awx/ui/client/src/instance-groups/main.js b/awx/ui/client/src/instance-groups/main.js index c73e7069b8..024444c7d1 100644 --- a/awx/ui/client/src/instance-groups/main.js +++ b/awx/ui/client/src/instance-groups/main.js @@ -1,35 +1,58 @@ import InstanceGroupsList from './list/instance-groups-list.controller'; -import instanceGroupsMultiselect from './instance-groups-multiselect/instance-groups.directive'; -import instanceGroupsModal from './instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive'; +import instanceGroupsMultiselect from '../shared/instance-groups-multiselect/instance-groups.directive'; +import instanceGroupsModal from '../shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive'; +import instanceGroupsRoute from './instance-groups.route'; +import instancesListRoute from './instances/instances-list.route'; +import JobsList from './jobs/jobs.list'; +import jobsListRoute from './jobs/jobs-list.route'; +import JobsListController from './jobs/jobs.controller'; +import InstanceList from './instances/instances.list'; +import instancesRoute from './instances/instances.route'; +import InstanceListController from './instances/instances.controller'; +import InstanceJobsList from './instances/instance-jobs/instance-jobs.list'; +import instanceJobsRoute from './instances/instance-jobs/instance-jobs.route'; +import instanceJobsListRoute from './instances/instance-jobs/instance-jobs-list.route'; +import InstanceJobsController from './instances/instance-jobs/instance-jobs.controller'; +import CapacityBar from './capacity-bar/main'; import list from './instance-groups.list'; import service from './instance-groups.service'; -import { N_ } from '../i18n'; export default -angular.module('instanceGroups', []) +angular.module('instanceGroups', [CapacityBar.name]) .service('InstanceGroupsService', service) .factory('InstanceGroupList', list) + .factory('JobsList', JobsList) + .factory('InstanceList', InstanceList) + .factory('InstanceJobsList', InstanceJobsList) .controller('InstanceGroupsList', InstanceGroupsList) + .controller('JobsListController', JobsListController) + .controller('InstanceListController', InstanceListController) + .controller('InstanceJobsController', InstanceJobsController) .directive('instanceGroupsMultiselect', instanceGroupsMultiselect) .directive('instanceGroupsModal', instanceGroupsModal) - .config(['$stateProvider', 'stateDefinitionsProvider', - function($stateProvider, stateDefinitionsProvider) { - let stateDefinitions = stateDefinitionsProvider.$get(); + .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', + function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { + let stateExtender = $stateExtenderProvider.$get(); + + + function generateInstanceGroupsStates() { + return new Promise((resolve) => { + resolve({ + states: [ + stateExtender.buildDefinition(instanceGroupsRoute), + stateExtender.buildDefinition(instancesRoute), + stateExtender.buildDefinition(instancesListRoute), + stateExtender.buildDefinition(jobsListRoute), + stateExtender.buildDefinition(instanceJobsRoute), + stateExtender.buildDefinition(instanceJobsListRoute) + ] + }); + }); + } $stateProvider.state({ name: 'instanceGroups', url: '/instance_groups', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'instanceGroups', - list: 'InstanceGroupList', - controllers: { - list: 'InstanceGroupsList' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_('INSTANCE GROUPS') - } - }) + lazyLoad: () => generateInstanceGroupsStates() }); - } - ]); \ No newline at end of file + }]); diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index a30f847d76..f4d6aade11 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -382,6 +382,8 @@ angular.module('GeneratorHelpers', [systemStatus.name]) html += "
diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups-multiselect.controller.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-multiselect.controller.js similarity index 100% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups-multiselect.controller.js rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-multiselect.controller.js diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less new file mode 100644 index 0000000000..77612ebf3b --- /dev/null +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less @@ -0,0 +1,22 @@ +@import "../../shared/branding/colors.default.less"; + +#InstanceGroups { + display: flex; + padding: 0 12px; +} + +#instance-groups-panel { + table { + overflow: hidden; + } + .List-header { + margin-bottom: 20px; + } + .isActive { + border-left: 10px solid @list-row-select-bord; + } + .instances-list, + .instance-jobs-list { + margin-top: 20px; + } +} diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.directive.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.directive.js similarity index 100% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.directive.js rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.directive.js diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html similarity index 68% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html index b4e4021205..2f4cb22322 100644 --- a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html @@ -1,6 +1,6 @@
- - @@ -11,7 +11,7 @@
- {{ tag.name }} + {{ tag.name | translate }}
From ae209af5667a3f65327d16ac4e444eea69ae6aa9 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 29 Jun 2017 12:09:35 -0400 Subject: [PATCH 17/69] Update PR based on review changes * Remove unnecessary translates * Extend show Refresh Button * Update smart search sort and page size params --- .../client/src/bread-crumb/bread-crumb.directive.js | 2 ++ .../client/src/bread-crumb/bread-crumb.partial.html | 2 +- .../src/instance-groups/instance-group.partial.html | 6 +++--- .../src/instance-groups/instance-groups.route.js | 11 +++++++++++ .../instance-jobs/instance-jobs-list.route.js | 9 +++++++++ .../instance-jobs/instance-jobs.partial.html | 6 +++--- .../instances/instances-list.partial.html | 6 +++--- .../instance-groups/instances/instances-list.route.js | 2 +- .../src/instance-groups/jobs/jobs-list.route.js | 7 ++++--- .../list/instance-groups-list.partial.html | 10 +++++----- .../instance-groups-modal.directive.js | 2 +- .../instance-groups-modal.partial.html | 2 +- .../instance-groups.directive.js | 2 +- .../instance-groups.partial.html | 2 +- 14 files changed, 46 insertions(+), 23 deletions(-) diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 86b604e2cc..93d17a00c9 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -12,6 +12,7 @@ export default scope.showActivityStreamButton = false; scope.showRefreshButton = false; + scope.alwaysShowRefreshButton = false; scope.loadingLicense = true; scope.$on("$stateChangeSuccess", function updateActivityStreamButton(event, toState, toParams, fromState, fromParams) { @@ -48,6 +49,7 @@ export default } scope.showRefreshButton = (streamConfig && streamConfig.refreshButton) ? true : false; + scope.alwaysShowRefreshButton = (streamConfig && streamConfig.alwaysShowRefreshButton) ? true: false; }); // scope.$on('featuresLoaded', function(){ diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html index a95c4d2996..606eec8b04 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html +++ b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html @@ -8,7 +8,7 @@ data-trigger="hover" data-container="body" ng-hide= "loadingLicense || licenseMissing" - ng-if="(socketStatus === 'error' && showRefreshButton) || $state.includes('instanceGroups')" + ng-if="(socketStatus === 'error' && showRefreshButton) || alwaysShowRefreshButton" ng-click="refresh()"> diff --git a/awx/ui/client/src/instance-groups/instance-group.partial.html b/awx/ui/client/src/instance-groups/instance-group.partial.html index cb8724a85b..9fe3b98d34 100644 --- a/awx/ui/client/src/instance-groups/instance-group.partial.html +++ b/awx/ui/client/src/instance-groups/instance-group.partial.html @@ -3,18 +3,18 @@
-
{{ instanceGroupName | translate }}
+
{{ instanceGroupName }}

Capacity

- {{ instanceGroupCapacity | translate }}% + {{ instanceGroupCapacity }}%

Running Jobs

- {{ instanceGroupJobsRunning | translate}} + {{ instanceGroupJobsRunning }}
diff --git a/awx/ui/client/src/instance-groups/instance-groups.route.js b/awx/ui/client/src/instance-groups/instance-groups.route.js index b9ed461fa3..89cd484dbb 100644 --- a/awx/ui/client/src/instance-groups/instance-groups.route.js +++ b/awx/ui/client/src/instance-groups/instance-groups.route.js @@ -9,6 +9,17 @@ export default { parent: 'setup', label: N_('INSTANCE GROUPS') }, + params: { + instance_group_search: { + value: { + page_size: '10', + order_by: 'name' + } + } + }, + data: { + alwaysShowRefreshButton: true, + }, views: { '@': { templateUrl: templateUrl('./instance-groups/instance-groups'), diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js index cc66f4168d..dcff49f2d0 100644 --- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js @@ -8,6 +8,15 @@ export default { parent: 'instanceGroups.instances.list', label: N_('{{ breadcrumb.instance_name }}') }, + params: { + instance_job_search: { + value: { + page_size: '10', + order_by: '-finished', + not__launch_type: 'sync' + } + } + }, views: { 'list@instanceGroups.instances.list.job': { templateProvider: function(InstanceJobsList, generateList) { diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html index 20bf3e0fcd..163a2e25fe 100644 --- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html @@ -3,18 +3,18 @@
-
{{ instanceName | translate }}
+
{{ instanceName }}

Capacity

- {{ instanceCapacity | translate }}% + {{ instanceCapacity }}%

Running Jobs

- {{ instanceJobsRunning | translate }} + {{ instanceJobsRunning }}
diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html index 9342125735..ac75597b2a 100644 --- a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html +++ b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html @@ -27,13 +27,13 @@ -
{{ instance.hostname | translate }} + {{ instance.hostname }} - {{ instance.percent_capacity_remaining | translate }}% + {{ instance.percent_capacity_remaining }}% - {{ instance.jobs_running | translate }} + {{ instance.jobs_running }} diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.route.js b/awx/ui/client/src/instance-groups/instances/instances-list.route.js index 6b95b33bea..89e572f3d7 100644 --- a/awx/ui/client/src/instance-groups/instances/instances-list.route.js +++ b/awx/ui/client/src/instance-groups/instances/instances-list.route.js @@ -12,7 +12,7 @@ export default { params: { instance_search: { value: { - page_size: '5', + page_size: '10', order_by: 'hostname' } } diff --git a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js index d21aa8a669..7dc5230339 100644 --- a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js +++ b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js @@ -9,10 +9,11 @@ export default { label: N_('JOBS') }, params: { - instance_group_job_search: { + job_search: { value: { - page_size: '5', - order_by: 'name' + page_size: '10', + order_by: '-finished', + not__launch_type: 'sync' } }, instance_group_id: null diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html index 6692769205..b6d3679a57 100644 --- a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html @@ -4,7 +4,7 @@ INSTANCE GROUPS
- {{ instanceGroupCount | translate}} + {{ instanceGroupCount }}
@@ -38,15 +38,15 @@ - {{ instance_group.name | translate }} - {{ instance_group.instances | translate }} + {{ instance_group.name }} + {{ instance_group.instances }} - {{ instance_group.percent_capacity_remaining | translate }}% + {{ instance_group.percent_capacity_remaining }}% - {{ instance_group.jobs_running | translate }} + {{ instance_group.jobs_running }} diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js index 2d89cd9909..9f3ef11658 100644 --- a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js @@ -4,7 +4,7 @@ export default ['templateUrl', function(templateUrl) { scope: { instanceGroups: '=' }, - templateUrl: templateUrl('instance-groups/instance-groups-multiselect/instance-groups-modal/instance-groups-modal'), + templateUrl: templateUrl('shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal'), link: function(scope, element) { diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.partial.html b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.partial.html index 6c8d91fc45..c61af3ab8b 100644 --- a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.partial.html +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.partial.html @@ -11,7 +11,7 @@
- {{ tag.name | translate }} + {{ tag.name }}
From c343c0059171234dc682c43e62ee0ae2aee6bf88 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 30 Jun 2017 14:59:00 -0400 Subject: [PATCH 18/69] Added multi-select preview to group/host associate and instance groups modals. --- .../hosts-related-groups-associate.route.js | 2 +- .../group-nested-groups-associate.route.js | 2 +- .../group-nested-hosts-associate.route.js | 2 +- .../host-nested-groups-associate.route.js | 2 +- .../associate-groups.controller.js | 16 ++- .../instance-groups-modal.directive.js | 7 +- .../list-generator/list-generator.factory.js | 4 + awx/ui/client/src/shared/main.js | 2 + .../src/shared/multi-select-preview/main.js | 11 +++ .../multi-select-preview.block.less | 99 +++++++++++++++++++ .../multi-select-preview.controller.js | 21 ++++ .../multi-select-preview.directive.js | 20 ++++ .../multi-select-preview.partial.html | 19 ++++ 13 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 awx/ui/client/src/shared/multi-select-preview/main.js create mode 100644 awx/ui/client/src/shared/multi-select-preview/multi-select-preview.block.less create mode 100644 awx/ui/client/src/shared/multi-select-preview/multi-select-preview.controller.js create mode 100644 awx/ui/client/src/shared/multi-select-preview/multi-select-preview.directive.js create mode 100644 awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html diff --git a/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups-associate.route.js b/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups-associate.route.js index 169a9d6b7a..c1567200d2 100644 --- a/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups-associate.route.js +++ b/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups-associate.route.js @@ -13,7 +13,7 @@ export default { controller: function($scope, $q, GroupsService, $state){ $scope.associateGroups = function(selectedItems){ var deferred = $q.defer(); - return $q.all( _.map(selectedItems, (id) => GroupsService.associateHost({id: parseInt($state.params.host_id)}, id)) ) + return $q.all( _.map(selectedItems, (selectedItem) => GroupsService.associateHost({id: parseInt($state.params.host_id)}, selectedItem.id)) ) .then( () =>{ deferred.resolve(); }, (error) => { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-associate.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-associate.route.js index 6125e02da7..294dcf031c 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-associate.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-associate.route.js @@ -13,7 +13,7 @@ export default { controller: function($scope, $q, GroupsService, $state){ $scope.associateGroups = function(selectedItems){ var deferred = $q.defer(); - return $q.all( _.map(selectedItems, (id) => GroupsService.associateGroup({id: id}, $state.params.group_id)) ) + return $q.all( _.map(selectedItems, (selectedItem) => GroupsService.associateGroup({id: selectedItem.id}, $state.params.group_id)) ) .then( () =>{ deferred.resolve(); }, (error) => { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts-associate.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts-associate.route.js index df09d30d80..959055ad02 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts-associate.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts-associate.route.js @@ -13,7 +13,7 @@ export default { controller: function($scope, $q, GroupsService, $state){ $scope.associateHosts = function(selectedItems){ var deferred = $q.defer(); - return $q.all( _.map(selectedItems, (id) => GroupsService.associateHost({id: id}, $state.params.group_id)) ) + return $q.all( _.map(selectedItems, (selectedItem) => GroupsService.associateHost({id: selectedItem.id}, $state.params.group_id)) ) .then( () =>{ deferred.resolve(); }, (error) => { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related/nested-groups/host-nested-groups-associate.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related/nested-groups/host-nested-groups-associate.route.js index 2b9a865b18..d17a181687 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related/nested-groups/host-nested-groups-associate.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related/nested-groups/host-nested-groups-associate.route.js @@ -13,7 +13,7 @@ export default { controller: function($scope, $q, GroupsService, $state){ $scope.associateGroups = function(selectedItems){ var deferred = $q.defer(); - return $q.all( _.map(selectedItems, (id) => GroupsService.associateHost({id: parseInt($state.params.host_id)}, id)) ) + return $q.all( _.map(selectedItems, (selectedItem) => GroupsService.associateHost({id: parseInt($state.params.host_id)}, selectedItem.id)) ) .then( () =>{ deferred.resolve(); }, (error) => { diff --git a/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.controller.js b/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.controller.js index ec06428d96..f6f4fc4bed 100644 --- a/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.controller.js +++ b/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.controller.js @@ -35,6 +35,10 @@ list.multiSelect = true; list.fields.name.ngClick = 'linkoutGroup(associate_group.id)'; list.trackBy = 'associate_group.id'; + list.multiSelectPreview = { + selectedRows: 'selectedItems', + availableRows: 'associate_groups' + }; delete list.actions; delete list.fieldActions; delete list.fields.failed_hosts; @@ -58,9 +62,11 @@ $scope.$watchCollection('associate_groups', function () { if($scope.selectedItems) { $scope.associate_groups.forEach(function(row, i) { - if (_.includes($scope.selectedItems, row.id)) { - $scope.associate_groups[i].isSelected = true; - } + $scope.selectedItems.forEach(function(selectedItem) { + if(selectedItem.id === row.id) { + $scope.associate_groups[i].isSelected = true; + } + }); }); } }); @@ -72,14 +78,14 @@ let item = value.value; if (value.isSelected) { - $scope.selectedItems.push(item.id); + $scope.selectedItems.push(item); } else { // _.remove() Returns the new array of removed elements. // This will pull all the values out of the array that don't // match the deselected item effectively removing it $scope.selectedItems = _.remove($scope.selectedItems, function(selectedItem) { - return selectedItem !== item.id; + return selectedItem.id !== item.id; }); } }); diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js index 9f3ef11658..271ed97901 100644 --- a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive.js @@ -46,7 +46,10 @@ export default ['templateUrl', function(templateUrl) { instanceGroupList.listTitle = false; instanceGroupList.well = false; instanceGroupList.multiSelect = true; - instanceGroupList.multiSelectExtended = true; + instanceGroupList.multiSelectPreview = { + selectedRows: 'igTags', + availableRows: 'instance_groups' + }; delete instanceGroupList.fields.percent_capacity_remaining; delete instanceGroupList.fields.jobs_running; @@ -104,4 +107,4 @@ export default ['templateUrl', function(templateUrl) { }; }] }; -}]; \ No newline at end of file +}]; diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index a735c16ff7..e4ac25273e 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -163,6 +163,10 @@ export default ['$compile', 'Attr', 'Icon', html += "\n"; } + if (list.multiSelectPreview) { + html += ""; + } + if (options.instructions) { html += "
" + options.instructions + "
\n"; } else if (list.instructions) { diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 3fdf3bdf95..1f4d57cfc5 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -32,6 +32,7 @@ import directives from './directives'; import features from './features/main'; import orgAdminLookup from './org-admin-lookup/main'; import limitPanels from './limit-panels/main'; +import multiSelectPreview from './multi-select-preview/main'; import 'angular-duration-format'; export default @@ -61,6 +62,7 @@ angular.module('shared', [listGenerator.name, features.name, orgAdminLookup.name, limitPanels.name, + multiSelectPreview.name, require('angular-cookies'), 'angular-duration-format' ]) diff --git a/awx/ui/client/src/shared/multi-select-preview/main.js b/awx/ui/client/src/shared/multi-select-preview/main.js new file mode 100644 index 0000000000..1f50c494c0 --- /dev/null +++ b/awx/ui/client/src/shared/multi-select-preview/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import multiSelectPreview from './multi-select-preview.directive'; + +export default + angular.module('multiSelectPreview', []) + .directive('multiSelectPreview', multiSelectPreview); diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.block.less b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.block.less new file mode 100644 index 0000000000..e0cdc87bcf --- /dev/null +++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.block.less @@ -0,0 +1,99 @@ +@import '../branding/colors.default.less'; + +.MultiSelectPreview { + display: flex; + flex: 1 0 auto; + margin-bottom: 15px; + align-items: baseline; +} + +.MultiSelectPreview-selectedItems { + display: flex; + flex: 0 0 100%; + background-color: @default-no-items-bord; + border: 1px solid @default-border; + padding: 10px; + border-radius: 5px; +} + +.MultiSelectPreview-selectedItemsLabel, .MultiSelectPreview-label { + color: @default-interface-txt; + margin-right: 10px; +} + +.MultiSelectPreview-selectedItemsLabel { + flex: 0 0 80px; + line-height: 29px; +} + +.MultiSelectPreview-previewTags--outer { + flex: 1 0 auto; + max-width: ~"calc(100% - 140px)"; +} + +.MultiSelectPreview-previewTags--inner { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + +.MultiSelectPreview-previewTagContainer { + display: flex; +} + +.MultiSelectPreview-previewTagRevert { + flex: 0 0 60px; + line-height: 29px; +} + +.MultiSelectPreview-revertLink { + font-size: 12px; +} + +.MultiSelectPreview-previewTagContainerDelete { + background-color: @default-link; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + color: @default-bg; + padding: 0 5px; + margin: 4px 0px; + align-items: center; + display: flex; + cursor: pointer; +} + +.MultiSelectPreview-previewTagContainerDelete:hover { + border-color: @default-err; + background-color: @default-err; +} + +.MultiSelectPreview-previewTagContainerDelete:hover > .MultiSelectPreview-previewTagContainerTagDelete { + color: @default-bg; +} + +.MultiSelectPreview-previewTag { + border-radius: 5px; + padding: 2px 10px; + margin: 4px 0px; + font-size: 12px; + color: @default-interface-txt; + background-color: @default-list-header-bg; + margin-right: 5px; + max-width: 100%; + display: inline-block; +} + +.MultiSelectPreview-previewTagLabel { + color: @default-list-header-bg; +} + +.MultiSelectPreview-previewTag--deletable { + color: @default-bg; + background-color: @default-link; + margin-right: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-right: 0; + max-width: ~"calc(100% - 23px)"; + margin-right: 5px; +} diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.controller.js b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.controller.js new file mode 100644 index 0000000000..be166234c7 --- /dev/null +++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.controller.js @@ -0,0 +1,21 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', + function ($scope) { + $scope.unselectSelectedRow = function(index) { + + angular.forEach($scope.availableRows, function(value) { + if(value.id === $scope.selectedRows[index].id) { + value.isSelected = false; + } + }); + + $scope.selectedRows.splice(index, 1); + + }; + } +]; diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.directive.js b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.directive.js new file mode 100644 index 0000000000..6805840f19 --- /dev/null +++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.directive.js @@ -0,0 +1,20 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import MultiSelectPreviewController from './multi-select-preview.controller'; + + export default ['templateUrl', function(templateUrl) { + return { + restrict: 'E', + replace: true, + scope: { + selectedRows: '=', + availableRows: '=' + }, + controller: MultiSelectPreviewController, + templateUrl: templateUrl('shared/multi-select-preview/multi-select-preview') + }; + }]; diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html new file mode 100644 index 0000000000..ba96157381 --- /dev/null +++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html @@ -0,0 +1,19 @@ +
+
+
+ SELECTED: +
+
+
+
+
+ +
+
+ {{selectedRow.name}} +
+
+
+
+
+
From 0487de88e44dc34dd7bbbfeab6ee326238571d8a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 30 Jun 2017 15:06:51 -0400 Subject: [PATCH 19/69] Make group and host plural --- .../shared/associate-groups/associate-groups.partial.html | 2 +- .../shared/associate-hosts/associate-hosts.partial.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.partial.html b/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.partial.html index 02a493af8c..54c3533f25 100644 --- a/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.partial.html +++ b/awx/ui/client/src/inventories-hosts/shared/associate-groups/associate-groups.partial.html @@ -3,7 +3,7 @@